Browse Source

Unix line endings

arkon 5 years ago
parent
commit
c4dad1c20b
96 changed files with 9761 additions and 9737 deletions
  1. 24 0
      .gitattributes
  2. 22 22
      app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt
  3. 33 33
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt
  4. 132 132
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
  5. 36 36
      app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt
  6. 70 70
      app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt
  7. 214 214
      app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
  8. 286 286
      app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
  9. 57 57
      app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt
  10. 9 9
      app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt
  11. 144 144
      app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt
  12. 16 16
      app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt
  13. 144 144
      app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
  14. 10 10
      app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt
  15. 164 164
      app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
  16. 13 13
      app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt
  17. 139 139
      app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt
  18. 154 154
      app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt
  19. 117 117
      app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
  20. 69 69
      app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
  21. 4 4
      app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt
  22. 39 39
      app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt
  23. 32 32
      app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
  24. 45 45
      app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt
  25. 43 43
      app/src/main/java/eu/kanade/tachiyomi/source/Source.kt
  26. 74 74
      app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
  27. 39 39
      app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
  28. 6 6
      app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
  29. 2 2
      app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt
  30. 48 48
      app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt
  31. 30 30
      app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
  32. 14 14
      app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt
  33. 57 57
      app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
  34. 22 22
      app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt
  35. 367 367
      app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
  36. 25 25
      app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt
  37. 14 14
      app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt
  38. 200 200
      app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt
  39. 21 21
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
  40. 61 61
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
  41. 44 44
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java
  42. 88 88
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt
  43. 51 51
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt
  44. 27 27
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
  45. 51 51
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
  46. 34 34
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
  47. 247 247
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt
  48. 47 47
      app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt
  49. 42 42
      app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt
  50. 102 102
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt
  51. 44 44
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
  52. 247 247
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
  53. 523 523
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
  54. 57 57
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
  55. 27 27
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
  56. 72 72
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
  57. 65 65
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
  58. 216 216
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt
  59. 371 371
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  60. 10 10
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt
  61. 31 31
      app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt
  62. 282 282
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  63. 193 193
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  64. 122 122
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt
  65. 52 52
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt
  66. 45 45
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt
  67. 486 486
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt
  68. 418 418
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  69. 31 31
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt
  70. 26 26
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt
  71. 41 41
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt
  72. 42 42
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt
  73. 42 42
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt
  74. 577 577
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt
  75. 173 173
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
  76. 73 73
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt
  77. 79 79
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt
  78. 57 57
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt
  79. 45 45
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt
  80. 141 141
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt
  81. 42 42
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt
  82. 6 6
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt
  83. 129 129
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt
  84. 78 78
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt
  85. 144 144
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt
  86. 333 333
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt
  87. 86 86
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt
  88. 60 60
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
  89. 238 238
      app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
  90. 7 7
      app/src/main/res/drawable/empty_drawable_32dp.xml
  91. 9 9
      app/src/main/res/drawable/ic_done_white_18dp.xml
  92. 9 9
      app/src/main/res/drawable/ic_watch_later_black_24dp.xml
  93. 23 23
      app/src/main/res/layout/navigation_view_checkbox.xml
  94. 29 29
      app/src/main/res/layout/navigation_view_group.xml
  95. 61 61
      app/src/main/res/layout/pref_item_source.xml
  96. 190 190
      app/src/main/res/layout/track_item.xml

+ 24 - 0
.gitattributes

@@ -0,0 +1,24 @@
+* text=auto
+* text eol=lf
+
+# Windows forced line-endings
+/.idea/* text eol=crlf
+
+# Gradle wrapper
+*.jar binary
+
+# Images
+*.webp binary
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.gz binary
+*.zip binary
+*.7z binary
+*.ttf binary
+*.eot binary
+*.woff binary
+*.pyc binary
+*.swp binary

+ 22 - 22
app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt

@@ -1,23 +1,23 @@
-package eu.kanade.tachiyomi.data.backup.models
-
-import java.text.SimpleDateFormat
-import java.util.*
-
-/**
- * Json values
- */
-object Backup {
-    const val CURRENT_VERSION = 2
-    const val MANGA = "manga"
-    const val MANGAS = "mangas"
-    const val TRACK = "track"
-    const val CHAPTERS = "chapters"
-    const val CATEGORIES = "categories"
-    const val HISTORY = "history"
-    const val VERSION = "version"
-
-    fun getDefaultFilename(): String {
-        val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
-        return "tachiyomi_$date.json"
-    }
+package eu.kanade.tachiyomi.data.backup.models
+
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * Json values
+ */
+object Backup {
+    const val CURRENT_VERSION = 2
+    const val MANGA = "manga"
+    const val MANGAS = "mangas"
+    const val TRACK = "track"
+    const val CHAPTERS = "chapters"
+    const val CATEGORIES = "categories"
+    const val HISTORY = "history"
+    const val VERSION = "version"
+
+    fun getDefaultFilename(): String {
+        val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
+        return "tachiyomi_$date.json"
+    }
 }

+ 33 - 33
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt

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

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

@@ -1,132 +1,132 @@
-package eu.kanade.tachiyomi.data.preference
-
-/**
- * This class stores the keys for the preferences in the application.
- */
-object PreferenceKeys {
-
-    const val theme = "pref_theme_key"
-
-    const val rotation = "pref_rotation_type_key"
-
-    const val enableTransitions = "pref_enable_transitions_key"
-
-    const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
-
-    const val showPageNumber = "pref_show_page_number_key"
-
-    const val trueColor = "pref_true_color_key"
-
-    const val fullscreen = "fullscreen"
-
-    const val keepScreenOn = "pref_keep_screen_on_key"
-
-    const val customBrightness = "pref_custom_brightness_key"
-
-    const val customBrightnessValue = "custom_brightness_value"
-
-    const val colorFilter = "pref_color_filter_key"
-
-    const val colorFilterValue = "color_filter_value"
-
-    const val colorFilterMode = "color_filter_mode"
-
-    const val defaultViewer = "pref_default_viewer_key"
-
-    const val imageScaleType = "pref_image_scale_type_key"
-
-    const val zoomStart = "pref_zoom_start_key"
-
-    const val readerTheme = "pref_reader_theme_key"
-
-    const val cropBorders = "crop_borders"
-
-    const val cropBordersWebtoon = "crop_borders_webtoon"
-
-    const val readWithTapping = "reader_tap"
-
-    const val readWithLongTap = "reader_long_tap"
-
-    const val readWithVolumeKeys = "reader_volume_keys"
-
-    const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
-
-    const val portraitColumns = "pref_library_columns_portrait_key"
-
-    const val landscapeColumns = "pref_library_columns_landscape_key"
-
-    const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
-
-    const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
-
-    const val lastUsedCatalogueSource = "last_catalogue_source"
-
-    const val lastUsedCategory = "last_used_category"
-
-    const val catalogueAsList = "pref_display_catalogue_as_list"
-
-    const val enabledLanguages = "source_languages"
-
-    const val backupDirectory = "backup_directory"
-
-    const val downloadsDirectory = "download_directory"
-
-    const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
-
-    const val numberOfBackups = "backup_slots"
-
-    const val backupInterval = "backup_interval"
-
-    const val removeAfterReadSlots = "remove_after_read_slots"
-
-    const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
-
-    const val libraryUpdateInterval = "pref_library_update_interval_key"
-
-    const val libraryUpdateRestriction = "library_update_restriction"
-
-    const val libraryUpdateCategories = "library_update_categories"
-
-    const val libraryUpdatePrioritization = "library_update_prioritization"
-
-    const val filterDownloaded = "pref_filter_downloaded_key"
-
-    const val filterUnread = "pref_filter_unread_key"
-
-    const val filterCompleted = "pref_filter_completed_key"
-
-    const val librarySortingMode = "library_sorting_mode"
-
-    const val automaticUpdates = "automatic_updates"
-
-    const val startScreen = "start_screen"
-
-    const val downloadNew = "download_new"
-
-    const val downloadNewCategories = "download_new_categories"
-
-    const val libraryAsList = "pref_display_library_as_list"
-
-    const val lang = "app_language"
-
-    const val defaultCategory = "default_category"
-
-    const val skipRead = "skip_read"
-
-    const val downloadBadge = "display_download_badge"
-
-    @Deprecated("Use the preferences of the source")
-    fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
-
-    @Deprecated("Use the preferences of the source")
-    fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
-
-    fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
-
-    fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
-
-    fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
-
-    fun trackToken(syncId: Int) = "track_token_$syncId"
-
-}
+package eu.kanade.tachiyomi.data.preference
+
+/**
+ * This class stores the keys for the preferences in the application.
+ */
+object PreferenceKeys {
+
+    const val theme = "pref_theme_key"
+
+    const val rotation = "pref_rotation_type_key"
+
+    const val enableTransitions = "pref_enable_transitions_key"
+
+    const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
+
+    const val showPageNumber = "pref_show_page_number_key"
+
+    const val trueColor = "pref_true_color_key"
+
+    const val fullscreen = "fullscreen"
+
+    const val keepScreenOn = "pref_keep_screen_on_key"
+
+    const val customBrightness = "pref_custom_brightness_key"
+
+    const val customBrightnessValue = "custom_brightness_value"
+
+    const val colorFilter = "pref_color_filter_key"
+
+    const val colorFilterValue = "color_filter_value"
+
+    const val colorFilterMode = "color_filter_mode"
+
+    const val defaultViewer = "pref_default_viewer_key"
+
+    const val imageScaleType = "pref_image_scale_type_key"
+
+    const val zoomStart = "pref_zoom_start_key"
+
+    const val readerTheme = "pref_reader_theme_key"
+
+    const val cropBorders = "crop_borders"
+
+    const val cropBordersWebtoon = "crop_borders_webtoon"
+
+    const val readWithTapping = "reader_tap"
+
+    const val readWithLongTap = "reader_long_tap"
+
+    const val readWithVolumeKeys = "reader_volume_keys"
+
+    const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
+
+    const val portraitColumns = "pref_library_columns_portrait_key"
+
+    const val landscapeColumns = "pref_library_columns_landscape_key"
+
+    const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
+
+    const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
+
+    const val lastUsedCatalogueSource = "last_catalogue_source"
+
+    const val lastUsedCategory = "last_used_category"
+
+    const val catalogueAsList = "pref_display_catalogue_as_list"
+
+    const val enabledLanguages = "source_languages"
+
+    const val backupDirectory = "backup_directory"
+
+    const val downloadsDirectory = "download_directory"
+
+    const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
+
+    const val numberOfBackups = "backup_slots"
+
+    const val backupInterval = "backup_interval"
+
+    const val removeAfterReadSlots = "remove_after_read_slots"
+
+    const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
+
+    const val libraryUpdateInterval = "pref_library_update_interval_key"
+
+    const val libraryUpdateRestriction = "library_update_restriction"
+
+    const val libraryUpdateCategories = "library_update_categories"
+
+    const val libraryUpdatePrioritization = "library_update_prioritization"
+
+    const val filterDownloaded = "pref_filter_downloaded_key"
+
+    const val filterUnread = "pref_filter_unread_key"
+
+    const val filterCompleted = "pref_filter_completed_key"
+
+    const val librarySortingMode = "library_sorting_mode"
+
+    const val automaticUpdates = "automatic_updates"
+
+    const val startScreen = "start_screen"
+
+    const val downloadNew = "download_new"
+
+    const val downloadNewCategories = "download_new_categories"
+
+    const val libraryAsList = "pref_display_library_as_list"
+
+    const val lang = "app_language"
+
+    const val defaultCategory = "default_category"
+
+    const val skipRead = "skip_read"
+
+    const val downloadBadge = "display_download_badge"
+
+    @Deprecated("Use the preferences of the source")
+    fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
+
+    @Deprecated("Use the preferences of the source")
+    fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
+
+    fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
+
+    fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
+
+    fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
+
+    fun trackToken(syncId: Int) = "track_token_$syncId"
+
+}

+ 36 - 36
app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt

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

+ 70 - 70
app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt

@@ -1,70 +1,70 @@
-package eu.kanade.tachiyomi.data.track
-
-import android.support.annotation.CallSuper
-import android.support.annotation.DrawableRes
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.network.NetworkHelper
-import okhttp3.OkHttpClient
-import rx.Completable
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-
-abstract class TrackService(val id: Int) {
-
-    val preferences: PreferencesHelper by injectLazy()
-    val networkService: NetworkHelper by injectLazy()
-
-    open val client: OkHttpClient
-        get() = networkService.client
-
-    // Name of the manga sync service to display
-    abstract val name: String
-
-    @DrawableRes
-    abstract fun getLogo(): Int
-
-    abstract fun getLogoColor(): Int
-
-    abstract fun getStatusList(): List<Int>
-
-    abstract fun getStatus(status: Int): String
-
-    abstract fun getScoreList(): List<String>
-
-    open fun indexToScore(index: Int): Float {
-        return index.toFloat()
-    }
-
-    abstract fun displayScore(track: Track): String
-
-    abstract fun add(track: Track): Observable<Track>
-
-    abstract fun update(track: Track): Observable<Track>
-
-    abstract fun bind(track: Track): Observable<Track>
-
-    abstract fun search(query: String): Observable<List<TrackSearch>>
-
-    abstract fun refresh(track: Track): Observable<Track>
-
-    abstract fun login(username: String, password: String): Completable
-
-    @CallSuper
-    open fun logout() {
-        preferences.setTrackCredentials(this, "", "")
-    }
-
-    open val isLogged: Boolean
-        get() = !getUsername().isEmpty() &&
-                !getPassword().isEmpty()
-
-    fun getUsername() = preferences.trackUsername(this)!!
-
-    fun getPassword() = preferences.trackPassword(this)!!
-
-    fun saveCredentials(username: String, password: String) {
-        preferences.setTrackCredentials(this, username, password)
-    }
-}
+package eu.kanade.tachiyomi.data.track
+
+import android.support.annotation.CallSuper
+import android.support.annotation.DrawableRes
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
+import okhttp3.OkHttpClient
+import rx.Completable
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+
+abstract class TrackService(val id: Int) {
+
+    val preferences: PreferencesHelper by injectLazy()
+    val networkService: NetworkHelper by injectLazy()
+
+    open val client: OkHttpClient
+        get() = networkService.client
+
+    // Name of the manga sync service to display
+    abstract val name: String
+
+    @DrawableRes
+    abstract fun getLogo(): Int
+
+    abstract fun getLogoColor(): Int
+
+    abstract fun getStatusList(): List<Int>
+
+    abstract fun getStatus(status: Int): String
+
+    abstract fun getScoreList(): List<String>
+
+    open fun indexToScore(index: Int): Float {
+        return index.toFloat()
+    }
+
+    abstract fun displayScore(track: Track): String
+
+    abstract fun add(track: Track): Observable<Track>
+
+    abstract fun update(track: Track): Observable<Track>
+
+    abstract fun bind(track: Track): Observable<Track>
+
+    abstract fun search(query: String): Observable<List<TrackSearch>>
+
+    abstract fun refresh(track: Track): Observable<Track>
+
+    abstract fun login(username: String, password: String): Completable
+
+    @CallSuper
+    open fun logout() {
+        preferences.setTrackCredentials(this, "", "")
+    }
+
+    open val isLogged: Boolean
+        get() = !getUsername().isEmpty() &&
+                !getPassword().isEmpty()
+
+    fun getUsername() = preferences.trackUsername(this)!!
+
+    fun getPassword() = preferences.trackPassword(this)!!
+
+    fun saveCredentials(username: String, password: String) {
+        preferences.setTrackCredentials(this, username, password)
+    }
+}

+ 214 - 214
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt

@@ -1,214 +1,214 @@
-package eu.kanade.tachiyomi.data.track.anilist
-
-import android.content.Context
-import android.graphics.Color
-import com.google.gson.Gson
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import rx.Completable
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-
-class Anilist(private val context: Context, id: Int) : TrackService(id) {
-
-    companion object {
-        const val READING = 1
-        const val COMPLETED = 2
-        const val ON_HOLD = 3
-        const val DROPPED = 4
-        const val PLANNING = 5
-        const val REPEATING = 6
-
-        const val DEFAULT_STATUS = READING
-        const val DEFAULT_SCORE = 0
-
-        const val POINT_100 = "POINT_100"
-        const val POINT_10 = "POINT_10"
-        const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
-        const val POINT_5 = "POINT_5"
-        const val POINT_3 = "POINT_3"
-    }
-
-    override val name = "AniList"
-
-    private val gson: Gson by injectLazy()
-
-    private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
-
-    private val api by lazy { AnilistApi(client, interceptor) }
-
-    private val scorePreference = preferences.anilistScoreType()
-
-    init {
-        // If the preference is an int from APIv1, logout user to force using APIv2
-        try {
-            scorePreference.get()
-        } catch (e: ClassCastException) {
-            logout()
-            scorePreference.delete()
-        }
-    }
-
-    override fun getLogo() = R.drawable.al
-
-    override fun getLogoColor() = Color.rgb(18, 25, 35)
-
-    override fun getStatusList(): List<Int> {
-        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
-    }
-
-    override fun getStatus(status: Int): String = with(context) {
-        when (status) {
-            READING -> getString(R.string.reading)
-            COMPLETED -> getString(R.string.completed)
-            ON_HOLD -> getString(R.string.on_hold)
-            DROPPED -> getString(R.string.dropped)
-            PLANNING -> getString(R.string.plan_to_read)
-            REPEATING -> getString(R.string.repeating)
-            else -> ""
-        }
-    }
-
-    override fun getScoreList(): List<String> {
-        return when (scorePreference.getOrDefault()) {
-            // 10 point
-            POINT_10 -> IntRange(0, 10).map(Int::toString)
-            // 100 point
-            POINT_100 -> IntRange(0, 100).map(Int::toString)
-            // 5 stars
-            POINT_5 -> IntRange(0, 5).map { "$it ★" }
-            // Smiley
-            POINT_3 -> listOf("-", "😦", "😐", "😊")
-            // 10 point decimal
-            POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
-            else -> throw Exception("Unknown score type")
-        }
-    }
-
-    override fun indexToScore(index: Int): Float {
-        return when (scorePreference.getOrDefault()) {
-            // 10 point
-            POINT_10 -> index * 10f
-            // 100 point
-            POINT_100 -> index.toFloat()
-            // 5 stars
-            POINT_5 -> when {
-                index == 0 -> 0f
-                else -> index * 20f - 10f
-            }
-            // Smiley
-            POINT_3 -> when {
-                index == 0 -> 0f
-                else -> index * 25f + 10f
-            }
-            // 10 point decimal
-            POINT_10_DECIMAL -> index.toFloat()
-            else -> throw Exception("Unknown score type")
-        }
-    }
-
-    override fun displayScore(track: Track): String {
-        val score = track.score
-
-        return when (scorePreference.getOrDefault()) {
-            POINT_5 -> when {
-                score == 0f -> "0 ★"
-                else -> "${((score + 10) / 20).toInt()} ★"
-            }
-            POINT_3 -> when {
-                score == 0f -> "0"
-                score <= 35 -> "😦"
-                score <= 60 -> "😐"
-                else -> "😊"
-            }
-            else -> track.toAnilistScore()
-        }
-    }
-
-    override fun add(track: Track): Observable<Track> {
-        return api.addLibManga(track)
-    }
-
-    override fun update(track: Track): Observable<Track> {
-        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
-            track.status = COMPLETED
-        }
-        // If user was using API v1 fetch library_id
-        if (track.library_id == null || track.library_id!! == 0L){
-            return api.findLibManga(track, getUsername().toInt()).flatMap {
-                if (it == null) {
-                    throw Exception("$track not found on user library")
-                }
-                track.library_id = it.library_id
-                api.updateLibManga(track)
-            }
-        }
-
-        return api.updateLibManga(track)
-    }
-
-    override fun bind(track: Track): Observable<Track> {
-        return api.findLibManga(track, getUsername().toInt())
-                .flatMap { remoteTrack ->
-                    if (remoteTrack != null) {
-                        track.copyPersonalFrom(remoteTrack)
-                        track.library_id = remoteTrack.library_id
-                        update(track)
-                    } else {
-                        // Set default fields if it's not found in the list
-                        track.score = DEFAULT_SCORE.toFloat()
-                        track.status = DEFAULT_STATUS
-                        add(track)
-                    }
-                }
-    }
-
-    override fun search(query: String): Observable<List<TrackSearch>> {
-        return api.search(query)
-    }
-
-    override fun refresh(track: Track): Observable<Track> {
-        return api.getLibManga(track, getUsername().toInt())
-                .map { remoteTrack ->
-                    track.copyPersonalFrom(remoteTrack)
-                    track.total_chapters = remoteTrack.total_chapters
-                    track
-                }
-    }
-
-    override fun login(username: String, password: String) = login(password)
-
-    fun login(token: String): Completable {
-        val oauth = api.createOAuth(token)
-        interceptor.setAuth(oauth)
-        return api.getCurrentUser().map { (username, scoreType) ->
-            scorePreference.set(scoreType)
-            saveCredentials(username.toString(), oauth.access_token)
-         }.doOnError{
-            logout()
-        }.toCompletable()
-    }
-
-    override fun logout() {
-        super.logout()
-        preferences.trackToken(this).set(null)
-        interceptor.setAuth(null)
-    }
-
-    fun saveOAuth(oAuth: OAuth?) {
-        preferences.trackToken(this).set(gson.toJson(oAuth))
-    }
-
-    fun loadOAuth(): OAuth? {
-        return try {
-            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
-        } catch (e: Exception) {
-            null
-        }
-    }
-
-}
-
+package eu.kanade.tachiyomi.data.track.anilist
+
+import android.content.Context
+import android.graphics.Color
+import com.google.gson.Gson
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import rx.Completable
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+
+class Anilist(private val context: Context, id: Int) : TrackService(id) {
+
+    companion object {
+        const val READING = 1
+        const val COMPLETED = 2
+        const val ON_HOLD = 3
+        const val DROPPED = 4
+        const val PLANNING = 5
+        const val REPEATING = 6
+
+        const val DEFAULT_STATUS = READING
+        const val DEFAULT_SCORE = 0
+
+        const val POINT_100 = "POINT_100"
+        const val POINT_10 = "POINT_10"
+        const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
+        const val POINT_5 = "POINT_5"
+        const val POINT_3 = "POINT_3"
+    }
+
+    override val name = "AniList"
+
+    private val gson: Gson by injectLazy()
+
+    private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
+
+    private val api by lazy { AnilistApi(client, interceptor) }
+
+    private val scorePreference = preferences.anilistScoreType()
+
+    init {
+        // If the preference is an int from APIv1, logout user to force using APIv2
+        try {
+            scorePreference.get()
+        } catch (e: ClassCastException) {
+            logout()
+            scorePreference.delete()
+        }
+    }
+
+    override fun getLogo() = R.drawable.al
+
+    override fun getLogoColor() = Color.rgb(18, 25, 35)
+
+    override fun getStatusList(): List<Int> {
+        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
+    }
+
+    override fun getStatus(status: Int): String = with(context) {
+        when (status) {
+            READING -> getString(R.string.reading)
+            COMPLETED -> getString(R.string.completed)
+            ON_HOLD -> getString(R.string.on_hold)
+            DROPPED -> getString(R.string.dropped)
+            PLANNING -> getString(R.string.plan_to_read)
+            REPEATING -> getString(R.string.repeating)
+            else -> ""
+        }
+    }
+
+    override fun getScoreList(): List<String> {
+        return when (scorePreference.getOrDefault()) {
+            // 10 point
+            POINT_10 -> IntRange(0, 10).map(Int::toString)
+            // 100 point
+            POINT_100 -> IntRange(0, 100).map(Int::toString)
+            // 5 stars
+            POINT_5 -> IntRange(0, 5).map { "$it ★" }
+            // Smiley
+            POINT_3 -> listOf("-", "😦", "😐", "😊")
+            // 10 point decimal
+            POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
+            else -> throw Exception("Unknown score type")
+        }
+    }
+
+    override fun indexToScore(index: Int): Float {
+        return when (scorePreference.getOrDefault()) {
+            // 10 point
+            POINT_10 -> index * 10f
+            // 100 point
+            POINT_100 -> index.toFloat()
+            // 5 stars
+            POINT_5 -> when {
+                index == 0 -> 0f
+                else -> index * 20f - 10f
+            }
+            // Smiley
+            POINT_3 -> when {
+                index == 0 -> 0f
+                else -> index * 25f + 10f
+            }
+            // 10 point decimal
+            POINT_10_DECIMAL -> index.toFloat()
+            else -> throw Exception("Unknown score type")
+        }
+    }
+
+    override fun displayScore(track: Track): String {
+        val score = track.score
+
+        return when (scorePreference.getOrDefault()) {
+            POINT_5 -> when {
+                score == 0f -> "0 ★"
+                else -> "${((score + 10) / 20).toInt()} ★"
+            }
+            POINT_3 -> when {
+                score == 0f -> "0"
+                score <= 35 -> "😦"
+                score <= 60 -> "😐"
+                else -> "😊"
+            }
+            else -> track.toAnilistScore()
+        }
+    }
+
+    override fun add(track: Track): Observable<Track> {
+        return api.addLibManga(track)
+    }
+
+    override fun update(track: Track): Observable<Track> {
+        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
+            track.status = COMPLETED
+        }
+        // If user was using API v1 fetch library_id
+        if (track.library_id == null || track.library_id!! == 0L){
+            return api.findLibManga(track, getUsername().toInt()).flatMap {
+                if (it == null) {
+                    throw Exception("$track not found on user library")
+                }
+                track.library_id = it.library_id
+                api.updateLibManga(track)
+            }
+        }
+
+        return api.updateLibManga(track)
+    }
+
+    override fun bind(track: Track): Observable<Track> {
+        return api.findLibManga(track, getUsername().toInt())
+                .flatMap { remoteTrack ->
+                    if (remoteTrack != null) {
+                        track.copyPersonalFrom(remoteTrack)
+                        track.library_id = remoteTrack.library_id
+                        update(track)
+                    } else {
+                        // Set default fields if it's not found in the list
+                        track.score = DEFAULT_SCORE.toFloat()
+                        track.status = DEFAULT_STATUS
+                        add(track)
+                    }
+                }
+    }
+
+    override fun search(query: String): Observable<List<TrackSearch>> {
+        return api.search(query)
+    }
+
+    override fun refresh(track: Track): Observable<Track> {
+        return api.getLibManga(track, getUsername().toInt())
+                .map { remoteTrack ->
+                    track.copyPersonalFrom(remoteTrack)
+                    track.total_chapters = remoteTrack.total_chapters
+                    track
+                }
+    }
+
+    override fun login(username: String, password: String) = login(password)
+
+    fun login(token: String): Completable {
+        val oauth = api.createOAuth(token)
+        interceptor.setAuth(oauth)
+        return api.getCurrentUser().map { (username, scoreType) ->
+            scorePreference.set(scoreType)
+            saveCredentials(username.toString(), oauth.access_token)
+         }.doOnError{
+            logout()
+        }.toCompletable()
+    }
+
+    override fun logout() {
+        super.logout()
+        preferences.trackToken(this).set(null)
+        interceptor.setAuth(null)
+    }
+
+    fun saveOAuth(oAuth: OAuth?) {
+        preferences.trackToken(this).set(gson.toJson(oAuth))
+    }
+
+    fun loadOAuth(): OAuth? {
+        return try {
+            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
+        } catch (e: Exception) {
+            null
+        }
+    }
+
+}
+

+ 286 - 286
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt

@@ -1,286 +1,286 @@
-package eu.kanade.tachiyomi.data.track.anilist
-
-import android.net.Uri
-import com.github.salomonbrys.kotson.*
-import com.google.gson.JsonObject
-import com.google.gson.JsonParser
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import eu.kanade.tachiyomi.network.asObservableSuccess
-import okhttp3.MediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody
-import rx.Observable
-import java.util.Calendar
-
-
-class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
-
-    private val parser = JsonParser()
-    private val jsonMime = MediaType.parse("application/json; charset=utf-8")
-    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
-
-    fun addLibManga(track: Track): Observable<Track> {
-        val query = """
-            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
-                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { 
-                |   id 
-                |   status 
-                |} 
-            |}
-            |""".trimMargin()
-        val variables = jsonObject(
-                "mangaId" to track.media_id,
-                "progress" to track.last_chapter_read,
-                "status" to track.toAnilistStatus()
-        )
-        val payload = jsonObject(
-                "query" to query,
-                "variables" to variables
-        )
-        val body = RequestBody.create(jsonMime, payload.toString())
-        val request = Request.Builder()
-                .url(apiUrl)
-                .post(body)
-                .build()
-        return authClient.newCall(request)
-                .asObservableSuccess()
-                .map { netResponse ->
-                    val responseBody = netResponse.body()?.string().orEmpty()
-                    netResponse.close()
-                    if (responseBody.isEmpty()) {
-                        throw Exception("Null Response")
-                    }
-                    val response = parser.parse(responseBody).obj
-                    track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
-                    track
-                }
-    }
-
-    fun updateLibManga(track: Track): Observable<Track> {
-        val query = """
-            |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
-                |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
-                    |id
-                    |status
-                    |progress
-                |}
-            |}
-            |""".trimMargin()
-        val variables = jsonObject(
-                "listId" to track.library_id,
-                "progress" to track.last_chapter_read,
-                "status" to track.toAnilistStatus(),
-                "score" to track.score.toInt()
-        )
-        val payload = jsonObject(
-                "query" to query,
-                "variables" to variables
-        )
-        val body = RequestBody.create(jsonMime, payload.toString())
-        val request = Request.Builder()
-                .url(apiUrl)
-                .post(body)
-                .build()
-        return authClient.newCall(request)
-                .asObservableSuccess()
-                .map {
-                    track
-                }
-    }
-
-    fun search(search: String): Observable<List<TrackSearch>> {
-        val query = """
-            |query Search(${'$'}query: String) {
-                |Page (perPage: 50) {
-                    |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
-                        |id
-                        |title {
-                            |romaji
-                        |}
-                        |coverImage {
-                            |large
-                        |}
-                        |type
-                        |status
-                        |chapters
-                        |description
-                        |startDate {
-                            |year
-                            |month
-                            |day
-                        |}
-                    |}
-                |}
-            |}
-            |""".trimMargin()
-        val variables = jsonObject(
-                "query" to search
-        )
-        val payload = jsonObject(
-                "query" to query,
-                "variables" to variables
-        )
-        val body = RequestBody.create(jsonMime, payload.toString())
-        val request = Request.Builder()
-                .url(apiUrl)
-                .post(body)
-                .build()
-        return authClient.newCall(request)
-                .asObservableSuccess()
-                .map { netResponse ->
-                    val responseBody = netResponse.body()?.string().orEmpty()
-                    if (responseBody.isEmpty()) {
-                        throw Exception("Null Response")
-                    }
-                    val response = parser.parse(responseBody).obj
-                    val data = response["data"]!!.obj
-                    val page = data["Page"].obj
-                    val media = page["media"].array
-                    val entries = media.map { jsonToALManga(it.obj) }
-                    entries.map { it.toTrack() }
-                }
-    }
-
-
-    fun findLibManga(track: Track, userid: Int): Observable<Track?> {
-        val query = """
-            |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
-                |Page {
-                    |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
-                        |id
-                        |status
-                        |scoreRaw: score(format: POINT_100)
-                        |progress
-                        |media {
-                            |id
-                            |title {
-                                |romaji
-                            |}
-                            |coverImage {
-                                |large
-                            |}
-                            |type
-                            |status
-                            |chapters
-                            |description
-                            |startDate {
-                                |year
-                                |month
-                                |day
-                            |}
-                        |}
-                    |}
-                |}
-            |}
-            |""".trimMargin()
-        val variables = jsonObject(
-                "id" to userid,
-                "manga_id" to track.media_id
-        )
-        val payload = jsonObject(
-                "query" to query,
-                "variables" to variables
-        )
-        val body = RequestBody.create(jsonMime, payload.toString())
-        val request = Request.Builder()
-                .url(apiUrl)
-                .post(body)
-                .build()
-        return authClient.newCall(request)
-                .asObservableSuccess()
-                .map { netResponse ->
-                    val responseBody = netResponse.body()?.string().orEmpty()
-                    if (responseBody.isEmpty()) {
-                        throw Exception("Null Response")
-                    }
-                    val response = parser.parse(responseBody).obj
-                    val data = response["data"]!!.obj
-                    val page = data["Page"].obj
-                    val media = page["mediaList"].array
-                    val entries = media.map { jsonToALUserManga(it.obj) }
-                    entries.firstOrNull()?.toTrack()
-
-                }
-    }
-
-    fun getLibManga(track: Track, userid: Int): Observable<Track> {
-        return findLibManga(track, userid)
-                .map { it ?: throw Exception("Could not find manga") }
-    }
-
-    fun createOAuth(token: String): OAuth {
-        return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
-    }
-
-    fun getCurrentUser(): Observable<Pair<Int, String>> {
-        val query = """
-            |query User {
-                |Viewer {
-                    |id
-                    |mediaListOptions {
-                        |scoreFormat
-                    |}
-                |}
-            |}
-            |""".trimMargin()
-        val payload = jsonObject(
-                "query" to query
-        )
-        val body = RequestBody.create(jsonMime, payload.toString())
-        val request = Request.Builder()
-                .url(apiUrl)
-                .post(body)
-                .build()
-        return authClient.newCall(request)
-                .asObservableSuccess()
-                .map { netResponse ->
-                    val responseBody = netResponse.body()?.string().orEmpty()
-                    if (responseBody.isEmpty()) {
-                        throw Exception("Null Response")
-                    }
-                    val response = parser.parse(responseBody).obj
-                    val data = response["data"]!!.obj
-                    val viewer = data["Viewer"].obj
-                    Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
-                }
-    }
-
-    private fun jsonToALManga(struct: JsonObject): ALManga {
-        val date = try {
-            val date = Calendar.getInstance()
-            date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
-                    struct["startDate"]["day"].nullInt ?: 0)
-            date.timeInMillis
-        } catch (_: Exception) {
-            0L
-        }
-
-        return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
-                struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
-                date, struct["chapters"].nullInt ?: 0)
-    }
-
-    private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
-        return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
-    }
-
-    companion object {
-        private const val clientId = "385"
-        private const val clientUrl = "tachiyomi://anilist-auth"
-        private const val apiUrl = "https://graphql.anilist.co/"
-        private const val baseUrl = "https://anilist.co/api/v2/"
-        private const val baseMangaUrl = "https://anilist.co/manga/"
-
-        fun mangaUrl(mediaId: Int): String {
-            return baseMangaUrl + mediaId
-        }
-
-        fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
-                .appendQueryParameter("client_id", clientId)
-                .appendQueryParameter("response_type", "token")
-                .build()
-    }
-
-}
+package eu.kanade.tachiyomi.data.track.anilist
+
+import android.net.Uri
+import com.github.salomonbrys.kotson.*
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import rx.Observable
+import java.util.Calendar
+
+
+class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
+
+    private val parser = JsonParser()
+    private val jsonMime = MediaType.parse("application/json; charset=utf-8")
+    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
+
+    fun addLibManga(track: Track): Observable<Track> {
+        val query = """
+            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
+                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { 
+                |   id 
+                |   status 
+                |} 
+            |}
+            |""".trimMargin()
+        val variables = jsonObject(
+                "mangaId" to track.media_id,
+                "progress" to track.last_chapter_read,
+                "status" to track.toAnilistStatus()
+        )
+        val payload = jsonObject(
+                "query" to query,
+                "variables" to variables
+        )
+        val body = RequestBody.create(jsonMime, payload.toString())
+        val request = Request.Builder()
+                .url(apiUrl)
+                .post(body)
+                .build()
+        return authClient.newCall(request)
+                .asObservableSuccess()
+                .map { netResponse ->
+                    val responseBody = netResponse.body()?.string().orEmpty()
+                    netResponse.close()
+                    if (responseBody.isEmpty()) {
+                        throw Exception("Null Response")
+                    }
+                    val response = parser.parse(responseBody).obj
+                    track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
+                    track
+                }
+    }
+
+    fun updateLibManga(track: Track): Observable<Track> {
+        val query = """
+            |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
+                |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
+                    |id
+                    |status
+                    |progress
+                |}
+            |}
+            |""".trimMargin()
+        val variables = jsonObject(
+                "listId" to track.library_id,
+                "progress" to track.last_chapter_read,
+                "status" to track.toAnilistStatus(),
+                "score" to track.score.toInt()
+        )
+        val payload = jsonObject(
+                "query" to query,
+                "variables" to variables
+        )
+        val body = RequestBody.create(jsonMime, payload.toString())
+        val request = Request.Builder()
+                .url(apiUrl)
+                .post(body)
+                .build()
+        return authClient.newCall(request)
+                .asObservableSuccess()
+                .map {
+                    track
+                }
+    }
+
+    fun search(search: String): Observable<List<TrackSearch>> {
+        val query = """
+            |query Search(${'$'}query: String) {
+                |Page (perPage: 50) {
+                    |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
+                        |id
+                        |title {
+                            |romaji
+                        |}
+                        |coverImage {
+                            |large
+                        |}
+                        |type
+                        |status
+                        |chapters
+                        |description
+                        |startDate {
+                            |year
+                            |month
+                            |day
+                        |}
+                    |}
+                |}
+            |}
+            |""".trimMargin()
+        val variables = jsonObject(
+                "query" to search
+        )
+        val payload = jsonObject(
+                "query" to query,
+                "variables" to variables
+        )
+        val body = RequestBody.create(jsonMime, payload.toString())
+        val request = Request.Builder()
+                .url(apiUrl)
+                .post(body)
+                .build()
+        return authClient.newCall(request)
+                .asObservableSuccess()
+                .map { netResponse ->
+                    val responseBody = netResponse.body()?.string().orEmpty()
+                    if (responseBody.isEmpty()) {
+                        throw Exception("Null Response")
+                    }
+                    val response = parser.parse(responseBody).obj
+                    val data = response["data"]!!.obj
+                    val page = data["Page"].obj
+                    val media = page["media"].array
+                    val entries = media.map { jsonToALManga(it.obj) }
+                    entries.map { it.toTrack() }
+                }
+    }
+
+
+    fun findLibManga(track: Track, userid: Int): Observable<Track?> {
+        val query = """
+            |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
+                |Page {
+                    |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
+                        |id
+                        |status
+                        |scoreRaw: score(format: POINT_100)
+                        |progress
+                        |media {
+                            |id
+                            |title {
+                                |romaji
+                            |}
+                            |coverImage {
+                                |large
+                            |}
+                            |type
+                            |status
+                            |chapters
+                            |description
+                            |startDate {
+                                |year
+                                |month
+                                |day
+                            |}
+                        |}
+                    |}
+                |}
+            |}
+            |""".trimMargin()
+        val variables = jsonObject(
+                "id" to userid,
+                "manga_id" to track.media_id
+        )
+        val payload = jsonObject(
+                "query" to query,
+                "variables" to variables
+        )
+        val body = RequestBody.create(jsonMime, payload.toString())
+        val request = Request.Builder()
+                .url(apiUrl)
+                .post(body)
+                .build()
+        return authClient.newCall(request)
+                .asObservableSuccess()
+                .map { netResponse ->
+                    val responseBody = netResponse.body()?.string().orEmpty()
+                    if (responseBody.isEmpty()) {
+                        throw Exception("Null Response")
+                    }
+                    val response = parser.parse(responseBody).obj
+                    val data = response["data"]!!.obj
+                    val page = data["Page"].obj
+                    val media = page["mediaList"].array
+                    val entries = media.map { jsonToALUserManga(it.obj) }
+                    entries.firstOrNull()?.toTrack()
+
+                }
+    }
+
+    fun getLibManga(track: Track, userid: Int): Observable<Track> {
+        return findLibManga(track, userid)
+                .map { it ?: throw Exception("Could not find manga") }
+    }
+
+    fun createOAuth(token: String): OAuth {
+        return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
+    }
+
+    fun getCurrentUser(): Observable<Pair<Int, String>> {
+        val query = """
+            |query User {
+                |Viewer {
+                    |id
+                    |mediaListOptions {
+                        |scoreFormat
+                    |}
+                |}
+            |}
+            |""".trimMargin()
+        val payload = jsonObject(
+                "query" to query
+        )
+        val body = RequestBody.create(jsonMime, payload.toString())
+        val request = Request.Builder()
+                .url(apiUrl)
+                .post(body)
+                .build()
+        return authClient.newCall(request)
+                .asObservableSuccess()
+                .map { netResponse ->
+                    val responseBody = netResponse.body()?.string().orEmpty()
+                    if (responseBody.isEmpty()) {
+                        throw Exception("Null Response")
+                    }
+                    val response = parser.parse(responseBody).obj
+                    val data = response["data"]!!.obj
+                    val viewer = data["Viewer"].obj
+                    Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
+                }
+    }
+
+    private fun jsonToALManga(struct: JsonObject): ALManga {
+        val date = try {
+            val date = Calendar.getInstance()
+            date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
+                    struct["startDate"]["day"].nullInt ?: 0)
+            date.timeInMillis
+        } catch (_: Exception) {
+            0L
+        }
+
+        return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
+                struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
+                date, struct["chapters"].nullInt ?: 0)
+    }
+
+    private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
+        return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
+    }
+
+    companion object {
+        private const val clientId = "385"
+        private const val clientUrl = "tachiyomi://anilist-auth"
+        private const val apiUrl = "https://graphql.anilist.co/"
+        private const val baseUrl = "https://anilist.co/api/v2/"
+        private const val baseMangaUrl = "https://anilist.co/manga/"
+
+        fun mangaUrl(mediaId: Int): String {
+            return baseMangaUrl + mediaId
+        }
+
+        fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
+                .appendQueryParameter("client_id", clientId)
+                .appendQueryParameter("response_type", "token")
+                .build()
+    }
+
+}

+ 57 - 57
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt

@@ -1,58 +1,58 @@
-package eu.kanade.tachiyomi.data.track.anilist
-
-import okhttp3.Interceptor
-import okhttp3.Response
-
-
-class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
-
-    /**
-     * OAuth object used for authenticated requests.
-     *
-     * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
-     * before its original expiration date.
-     */
-    private var oauth: OAuth? = null
-        set(value) {
-            field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
-        }
-
-    override fun intercept(chain: Interceptor.Chain): Response {
-        val originalRequest = chain.request()
-
-        if (token.isNullOrEmpty()) {
-            throw Exception("Not authenticated with Anilist")
-        }
-        if (oauth == null){
-            oauth = anilist.loadOAuth()
-        }
-        // Refresh access token if null or expired.
-        if (oauth!!.isExpired()) {
-            anilist.logout()
-            throw Exception("Token expired")
-        }
-
-        // Throw on null auth.
-        if (oauth == null) {
-            throw Exception("No authentication token")
-        }
-
-        // Add the authorization header to the original request.
-        val authRequest = originalRequest.newBuilder()
-                .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
-                .build()
-
-        return chain.proceed(authRequest)
-    }
-
-    /**
-     * Called when the user authenticates with Anilist for the first time. Sets the refresh token
-     * and the oauth object.
-     */
-    fun setAuth(oauth: OAuth?) {
-        token = oauth?.access_token
-        this.oauth = oauth
-        anilist.saveOAuth(oauth)
-    }
-
+package eu.kanade.tachiyomi.data.track.anilist
+
+import okhttp3.Interceptor
+import okhttp3.Response
+
+
+class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
+
+    /**
+     * OAuth object used for authenticated requests.
+     *
+     * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
+     * before its original expiration date.
+     */
+    private var oauth: OAuth? = null
+        set(value) {
+            field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
+        }
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val originalRequest = chain.request()
+
+        if (token.isNullOrEmpty()) {
+            throw Exception("Not authenticated with Anilist")
+        }
+        if (oauth == null){
+            oauth = anilist.loadOAuth()
+        }
+        // Refresh access token if null or expired.
+        if (oauth!!.isExpired()) {
+            anilist.logout()
+            throw Exception("Token expired")
+        }
+
+        // Throw on null auth.
+        if (oauth == null) {
+            throw Exception("No authentication token")
+        }
+
+        // Add the authorization header to the original request.
+        val authRequest = originalRequest.newBuilder()
+                .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
+                .build()
+
+        return chain.proceed(authRequest)
+    }
+
+    /**
+     * Called when the user authenticates with Anilist for the first time. Sets the refresh token
+     * and the oauth object.
+     */
+    fun setAuth(oauth: OAuth?) {
+        token = oauth?.access_token
+        this.oauth = oauth
+        anilist.saveOAuth(oauth)
+    }
+
 }

+ 9 - 9
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt

@@ -1,10 +1,10 @@
-package eu.kanade.tachiyomi.data.track.anilist
-
-data class OAuth(
-        val access_token: String,
-        val token_type: String,
-        val expires: Long,
-        val expires_in: Long) {
-
-    fun isExpired() = System.currentTimeMillis() > expires
+package eu.kanade.tachiyomi.data.track.anilist
+
+data class OAuth(
+        val access_token: String,
+        val token_type: String,
+        val expires: Long,
+        val expires_in: Long) {
+
+    fun isExpired() = System.currentTimeMillis() > expires
 }

+ 144 - 144
app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt

@@ -1,144 +1,144 @@
-package eu.kanade.tachiyomi.data.track.bangumi
-
-import android.content.Context
-import android.graphics.Color
-import com.google.gson.Gson
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import rx.Completable
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-
-class Bangumi(private val context: Context, id: Int) : TrackService(id) {
-
-  override fun getScoreList(): List<String> {
-    return IntRange(0, 10).map(Int::toString)
-  }
-
-  override fun displayScore(track: Track): String {
-    return track.score.toInt().toString()
-  }
-
-  override fun add(track: Track): Observable<Track> {
-    return api.addLibManga(track)
-  }
-
-  override fun update(track: Track): Observable<Track> {
-    if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
-      track.status = COMPLETED
-    }
-    return api.updateLibManga(track)
-  }
-
-  override fun bind(track: Track): Observable<Track> {
-    return api.statusLibManga(track)
-      .flatMap {
-        api.findLibManga(track).flatMap { remoteTrack ->
-          if (remoteTrack != null && it != null) {
-            track.copyPersonalFrom(remoteTrack)
-            track.library_id = remoteTrack.library_id
-            track.status = remoteTrack.status
-            track.last_chapter_read = remoteTrack.last_chapter_read
-            update(track)
-          } else {
-            // Set default fields if it's not found in the list
-            track.score = DEFAULT_SCORE.toFloat()
-            track.status = DEFAULT_STATUS
-            add(track)
-            update(track)
-          }
-        }
-      }
-  }
-
-  override fun search(query: String): Observable<List<TrackSearch>> {
-    return api.search(query)
-  }
-
-  override fun refresh(track: Track): Observable<Track> {
-    return api.statusLibManga(track)
-      .flatMap {
-        track.copyPersonalFrom(it!!)
-        api.findLibManga(track)
-          .map { remoteTrack ->
-            if (remoteTrack != null) {
-              track.total_chapters = remoteTrack.total_chapters
-              track.status = remoteTrack.status
-            }
-            track
-          }
-      }
-  }
-
-  companion object {
-    const val READING = 3
-    const val COMPLETED = 2
-    const val ON_HOLD = 4
-    const val DROPPED = 5
-    const val PLANNING = 1
-
-    const val DEFAULT_STATUS = READING
-    const val DEFAULT_SCORE = 0
-  }
-
-  override val name = "Bangumi"
-
-  private val gson: Gson by injectLazy()
-
-  private val interceptor by lazy { BangumiInterceptor(this, gson) }
-
-  private val api by lazy { BangumiApi(client, interceptor) }
-
-  override fun getLogo() = R.drawable.bangumi
-
-  override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99)
-
-  override fun getStatusList(): List<Int> {
-    return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
-  }
-
-  override fun getStatus(status: Int): String = with(context) {
-    when (status) {
-      READING -> getString(R.string.reading)
-      COMPLETED -> getString(R.string.completed)
-      ON_HOLD -> getString(R.string.on_hold)
-      DROPPED -> getString(R.string.dropped)
-      PLANNING -> getString(R.string.plan_to_read)
-      else -> ""
-    }
-  }
-
-  override fun login(username: String, password: String) = login(password)
-
-  fun login(code: String): Completable {
-    return api.accessToken(code).map { oauth: OAuth? ->
-      interceptor.newAuth(oauth)
-      if (oauth != null) {
-        saveCredentials(oauth.user_id.toString(), oauth.access_token)
-      }
-    }.doOnError {
-      logout()
-    }.toCompletable()
-  }
-
-  fun saveToken(oauth: OAuth?) {
-    val json = gson.toJson(oauth)
-    preferences.trackToken(this).set(json)
-  }
-
-  fun restoreToken(): OAuth? {
-    return try {
-      gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
-    } catch (e: Exception) {
-      null
-    }
-  }
-
-  override fun logout() {
-    super.logout()
-    preferences.trackToken(this).set(null)
-    interceptor.newAuth(null)
-  }
-}
+package eu.kanade.tachiyomi.data.track.bangumi
+
+import android.content.Context
+import android.graphics.Color
+import com.google.gson.Gson
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import rx.Completable
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+
+class Bangumi(private val context: Context, id: Int) : TrackService(id) {
+
+  override fun getScoreList(): List<String> {
+    return IntRange(0, 10).map(Int::toString)
+  }
+
+  override fun displayScore(track: Track): String {
+    return track.score.toInt().toString()
+  }
+
+  override fun add(track: Track): Observable<Track> {
+    return api.addLibManga(track)
+  }
+
+  override fun update(track: Track): Observable<Track> {
+    if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
+      track.status = COMPLETED
+    }
+    return api.updateLibManga(track)
+  }
+
+  override fun bind(track: Track): Observable<Track> {
+    return api.statusLibManga(track)
+      .flatMap {
+        api.findLibManga(track).flatMap { remoteTrack ->
+          if (remoteTrack != null && it != null) {
+            track.copyPersonalFrom(remoteTrack)
+            track.library_id = remoteTrack.library_id
+            track.status = remoteTrack.status
+            track.last_chapter_read = remoteTrack.last_chapter_read
+            update(track)
+          } else {
+            // Set default fields if it's not found in the list
+            track.score = DEFAULT_SCORE.toFloat()
+            track.status = DEFAULT_STATUS
+            add(track)
+            update(track)
+          }
+        }
+      }
+  }
+
+  override fun search(query: String): Observable<List<TrackSearch>> {
+    return api.search(query)
+  }
+
+  override fun refresh(track: Track): Observable<Track> {
+    return api.statusLibManga(track)
+      .flatMap {
+        track.copyPersonalFrom(it!!)
+        api.findLibManga(track)
+          .map { remoteTrack ->
+            if (remoteTrack != null) {
+              track.total_chapters = remoteTrack.total_chapters
+              track.status = remoteTrack.status
+            }
+            track
+          }
+      }
+  }
+
+  companion object {
+    const val READING = 3
+    const val COMPLETED = 2
+    const val ON_HOLD = 4
+    const val DROPPED = 5
+    const val PLANNING = 1
+
+    const val DEFAULT_STATUS = READING
+    const val DEFAULT_SCORE = 0
+  }
+
+  override val name = "Bangumi"
+
+  private val gson: Gson by injectLazy()
+
+  private val interceptor by lazy { BangumiInterceptor(this, gson) }
+
+  private val api by lazy { BangumiApi(client, interceptor) }
+
+  override fun getLogo() = R.drawable.bangumi
+
+  override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99)
+
+  override fun getStatusList(): List<Int> {
+    return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
+  }
+
+  override fun getStatus(status: Int): String = with(context) {
+    when (status) {
+      READING -> getString(R.string.reading)
+      COMPLETED -> getString(R.string.completed)
+      ON_HOLD -> getString(R.string.on_hold)
+      DROPPED -> getString(R.string.dropped)
+      PLANNING -> getString(R.string.plan_to_read)
+      else -> ""
+    }
+  }
+
+  override fun login(username: String, password: String) = login(password)
+
+  fun login(code: String): Completable {
+    return api.accessToken(code).map { oauth: OAuth? ->
+      interceptor.newAuth(oauth)
+      if (oauth != null) {
+        saveCredentials(oauth.user_id.toString(), oauth.access_token)
+      }
+    }.doOnError {
+      logout()
+    }.toCompletable()
+  }
+
+  fun saveToken(oauth: OAuth?) {
+    val json = gson.toJson(oauth)
+    preferences.trackToken(this).set(json)
+  }
+
+  fun restoreToken(): OAuth? {
+    return try {
+      gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
+    } catch (e: Exception) {
+      null
+    }
+  }
+
+  override fun logout() {
+    super.logout()
+    preferences.trackToken(this).set(null)
+    interceptor.newAuth(null)
+  }
+}

+ 16 - 16
app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt

@@ -1,16 +1,16 @@
-package eu.kanade.tachiyomi.data.track.bangumi
-
-data class OAuth(
-  val access_token: String,
-  val token_type: String,
-  val created_at: Long,
-  val expires_in: Long,
-  val refresh_token: String?,
-  val user_id: Long?
-) {
-
-  // Access token refersh before expired
-  fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
-
-}
-
+package eu.kanade.tachiyomi.data.track.bangumi
+
+data class OAuth(
+  val access_token: String,
+  val token_type: String,
+  val created_at: Long,
+  val expires_in: Long,
+  val refresh_token: String?,
+  val user_id: Long?
+) {
+
+  // Access token refersh before expired
+  fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
+
+}
+

+ 144 - 144
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt

@@ -1,144 +1,144 @@
-package eu.kanade.tachiyomi.data.track.kitsu
-
-import android.content.Context
-import android.graphics.Color
-import com.google.gson.Gson
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import rx.Completable
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-import java.text.DecimalFormat
-
-class Kitsu(private val context: Context, id: Int) : TrackService(id) {
-
-    companion object {
-        const val READING = 1
-        const val COMPLETED = 2
-        const val ON_HOLD = 3
-        const val DROPPED = 4
-        const val PLAN_TO_READ = 5
-
-        const val DEFAULT_STATUS = READING
-        const val DEFAULT_SCORE = 0f
-    }
-
-    override val name = "Kitsu"
-
-    private val gson: Gson by injectLazy()
-
-    private val interceptor by lazy { KitsuInterceptor(this, gson) }
-
-    private val api by lazy { KitsuApi(client, interceptor) }
-
-    override fun getLogo(): Int {
-        return R.drawable.kitsu
-    }
-
-    override fun getLogoColor(): Int {
-        return Color.rgb(51, 37, 50)
-    }
-
-    override fun getStatusList(): List<Int> {
-        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
-    }
-
-    override fun getStatus(status: Int): String = with(context) {
-        when (status) {
-            READING -> getString(R.string.reading)
-            COMPLETED -> getString(R.string.completed)
-            ON_HOLD -> getString(R.string.on_hold)
-            DROPPED -> getString(R.string.dropped)
-            PLAN_TO_READ -> getString(R.string.plan_to_read)
-            else -> ""
-        }
-    }
-
-    override fun getScoreList(): List<String> {
-        val df = DecimalFormat("0.#")
-        return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
-    }
-
-    override fun indexToScore(index: Int): Float {
-        return if (index > 0) (index + 1) / 2f else 0f
-    }
-
-    override fun displayScore(track: Track): String {
-        val df = DecimalFormat("0.#")
-        return df.format(track.score)
-    }
-
-    override fun add(track: Track): Observable<Track> {
-        return api.addLibManga(track, getUserId())
-    }
-
-    override fun update(track: Track): Observable<Track> {
-        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
-            track.status = COMPLETED
-        }
-
-        return api.updateLibManga(track)
-    }
-
-    override fun bind(track: Track): Observable<Track> {
-        return api.findLibManga(track, getUserId())
-                .flatMap { remoteTrack ->
-                    if (remoteTrack != null) {
-                        track.copyPersonalFrom(remoteTrack)
-                        track.media_id = remoteTrack.media_id
-                        update(track)
-                    } else {
-                        track.score = DEFAULT_SCORE
-                        track.status = DEFAULT_STATUS
-                        add(track)
-                    }
-                }
-    }
-
-    override fun search(query: String): Observable<List<TrackSearch>> {
-        return api.search(query)
-    }
-
-    override fun refresh(track: Track): Observable<Track> {
-        return api.getLibManga(track)
-                .map { remoteTrack ->
-                    track.copyPersonalFrom(remoteTrack)
-                    track.total_chapters = remoteTrack.total_chapters
-                    track
-                }
-    }
-
-    override fun login(username: String, password: String): Completable {
-        return api.login(username, password)
-                .doOnNext { interceptor.newAuth(it) }
-                .flatMap { api.getCurrentUser() }
-                .doOnNext { userId -> saveCredentials(username, userId) }
-                .doOnError { logout() }
-                .toCompletable()
-    }
-
-    override fun logout() {
-        super.logout()
-        interceptor.newAuth(null)
-    }
-
-    private fun getUserId(): String {
-        return getPassword()
-    }
-
-    fun saveToken(oauth: OAuth?) {
-        val json = gson.toJson(oauth)
-        preferences.trackToken(this).set(json)
-    }
-
-    fun restoreToken(): OAuth? {
-        return try {
-            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
-        } catch (e: Exception) {
-            null
-        }
-    }
-
-}
+package eu.kanade.tachiyomi.data.track.kitsu
+
+import android.content.Context
+import android.graphics.Color
+import com.google.gson.Gson
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import rx.Completable
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.text.DecimalFormat
+
+class Kitsu(private val context: Context, id: Int) : TrackService(id) {
+
+    companion object {
+        const val READING = 1
+        const val COMPLETED = 2
+        const val ON_HOLD = 3
+        const val DROPPED = 4
+        const val PLAN_TO_READ = 5
+
+        const val DEFAULT_STATUS = READING
+        const val DEFAULT_SCORE = 0f
+    }
+
+    override val name = "Kitsu"
+
+    private val gson: Gson by injectLazy()
+
+    private val interceptor by lazy { KitsuInterceptor(this, gson) }
+
+    private val api by lazy { KitsuApi(client, interceptor) }
+
+    override fun getLogo(): Int {
+        return R.drawable.kitsu
+    }
+
+    override fun getLogoColor(): Int {
+        return Color.rgb(51, 37, 50)
+    }
+
+    override fun getStatusList(): List<Int> {
+        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
+    }
+
+    override fun getStatus(status: Int): String = with(context) {
+        when (status) {
+            READING -> getString(R.string.reading)
+            COMPLETED -> getString(R.string.completed)
+            ON_HOLD -> getString(R.string.on_hold)
+            DROPPED -> getString(R.string.dropped)
+            PLAN_TO_READ -> getString(R.string.plan_to_read)
+            else -> ""
+        }
+    }
+
+    override fun getScoreList(): List<String> {
+        val df = DecimalFormat("0.#")
+        return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
+    }
+
+    override fun indexToScore(index: Int): Float {
+        return if (index > 0) (index + 1) / 2f else 0f
+    }
+
+    override fun displayScore(track: Track): String {
+        val df = DecimalFormat("0.#")
+        return df.format(track.score)
+    }
+
+    override fun add(track: Track): Observable<Track> {
+        return api.addLibManga(track, getUserId())
+    }
+
+    override fun update(track: Track): Observable<Track> {
+        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
+            track.status = COMPLETED
+        }
+
+        return api.updateLibManga(track)
+    }
+
+    override fun bind(track: Track): Observable<Track> {
+        return api.findLibManga(track, getUserId())
+                .flatMap { remoteTrack ->
+                    if (remoteTrack != null) {
+                        track.copyPersonalFrom(remoteTrack)
+                        track.media_id = remoteTrack.media_id
+                        update(track)
+                    } else {
+                        track.score = DEFAULT_SCORE
+                        track.status = DEFAULT_STATUS
+                        add(track)
+                    }
+                }
+    }
+
+    override fun search(query: String): Observable<List<TrackSearch>> {
+        return api.search(query)
+    }
+
+    override fun refresh(track: Track): Observable<Track> {
+        return api.getLibManga(track)
+                .map { remoteTrack ->
+                    track.copyPersonalFrom(remoteTrack)
+                    track.total_chapters = remoteTrack.total_chapters
+                    track
+                }
+    }
+
+    override fun login(username: String, password: String): Completable {
+        return api.login(username, password)
+                .doOnNext { interceptor.newAuth(it) }
+                .flatMap { api.getCurrentUser() }
+                .doOnNext { userId -> saveCredentials(username, userId) }
+                .doOnError { logout() }
+                .toCompletable()
+    }
+
+    override fun logout() {
+        super.logout()
+        interceptor.newAuth(null)
+    }
+
+    private fun getUserId(): String {
+        return getPassword()
+    }
+
+    fun saveToken(oauth: OAuth?) {
+        val json = gson.toJson(oauth)
+        preferences.trackToken(this).set(json)
+    }
+
+    fun restoreToken(): OAuth? {
+        return try {
+            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
+        } catch (e: Exception) {
+            null
+        }
+    }
+
+}

+ 10 - 10
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt

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

+ 164 - 164
app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt

@@ -1,164 +1,164 @@
-package eu.kanade.tachiyomi.data.track.myanimelist
-
-import android.content.Context
-import android.graphics.Color
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import okhttp3.HttpUrl
-import rx.Completable
-import rx.Observable
-import java.lang.Exception
-
-class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
-
-    companion object {
-        const val READING = 1
-        const val COMPLETED = 2
-        const val ON_HOLD = 3
-        const val DROPPED = 4
-        const val PLAN_TO_READ = 6
-
-        const val DEFAULT_STATUS = READING
-        const val DEFAULT_SCORE = 0
-
-        const val BASE_URL = "https://myanimelist.net"
-        const val USER_SESSION_COOKIE = "MALSESSIONID"
-        const val LOGGED_IN_COOKIE = "is_logged_in"
-    }
-
-    private val interceptor by lazy { MyAnimeListInterceptor(this) }
-    private val api by lazy { MyanimelistApi(client, interceptor) }
-
-    override val name: String
-        get() = "MyAnimeList"
-
-    override fun getLogo() = R.drawable.mal
-
-    override fun getLogoColor() = Color.rgb(46, 81, 162)
-
-    override fun getStatus(status: Int): String = with(context) {
-        when (status) {
-            READING -> getString(R.string.reading)
-            COMPLETED -> getString(R.string.completed)
-            ON_HOLD -> getString(R.string.on_hold)
-            DROPPED -> getString(R.string.dropped)
-            PLAN_TO_READ -> getString(R.string.plan_to_read)
-            else -> ""
-        }
-    }
-
-    override fun getStatusList(): List<Int> {
-        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
-    }
-
-    override fun getScoreList(): List<String> {
-        return IntRange(0, 10).map(Int::toString)
-    }
-
-    override fun displayScore(track: Track): String {
-        return track.score.toInt().toString()
-    }
-
-    override fun add(track: Track): Observable<Track> {
-        return api.addLibManga(track)
-    }
-
-    override fun update(track: Track): Observable<Track> {
-        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
-            track.status = COMPLETED
-        }
-
-        return api.updateLibManga(track)
-    }
-
-    override fun bind(track: Track): Observable<Track> {
-        return api.findLibManga(track)
-                .flatMap { remoteTrack ->
-                    if (remoteTrack != null) {
-                        track.copyPersonalFrom(remoteTrack)
-                        update(track)
-                    } else {
-                        // Set default fields if it's not found in the list
-                        track.score = DEFAULT_SCORE.toFloat()
-                        track.status = DEFAULT_STATUS
-                        add(track)
-                    }
-                }
-    }
-
-    override fun search(query: String): Observable<List<TrackSearch>> {
-        return api.search(query)
-    }
-
-    override fun refresh(track: Track): Observable<Track> {
-        return api.getLibManga(track)
-                .map { remoteTrack ->
-                    track.copyPersonalFrom(remoteTrack)
-                    track.total_chapters = remoteTrack.total_chapters
-                    track
-                }
-    }
-
-    override fun login(username: String, password: String): Completable {
-        logout()
-
-        return Observable.fromCallable { api.login(username, password) }
-                .doOnNext { csrf -> saveCSRF(csrf) }
-                .doOnNext { saveCredentials(username, password) }
-                .doOnError { logout() }
-                .toCompletable()
-    }
-
-    fun refreshLogin() {
-        val username = getUsername()
-        val password = getPassword()
-        logout()
-
-        try {
-            val csrf = api.login(username, password)
-            saveCSRF(csrf)
-            saveCredentials(username, password)
-        } catch (e: Exception) {
-            logout()
-            throw e
-        }
-    }
-
-    // Attempt to login again if cookies have been cleared but credentials are still filled
-    fun ensureLoggedIn() {
-        if (isAuthorized) return
-        if (!isLogged) throw Exception("MAL Login Credentials not found")
-
-        refreshLogin()
-    }
-
-    override fun logout() {
-        super.logout()
-        preferences.trackToken(this).delete()
-        networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
-    }
-
-    val isAuthorized: Boolean
-        get() = super.isLogged &&
-                getCSRF().isNotEmpty() &&
-                checkCookies()
-
-    fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
-
-    private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
-
-    private fun checkCookies(): Boolean {
-        var ckCount = 0
-        val url = HttpUrl.parse(BASE_URL)!!
-        for (ck in networkService.cookieManager.get(url)) {
-            if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
-                ckCount++
-        }
-
-        return ckCount == 2
-    }
-
-}
+package eu.kanade.tachiyomi.data.track.myanimelist
+
+import android.content.Context
+import android.graphics.Color
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import okhttp3.HttpUrl
+import rx.Completable
+import rx.Observable
+import java.lang.Exception
+
+class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
+
+    companion object {
+        const val READING = 1
+        const val COMPLETED = 2
+        const val ON_HOLD = 3
+        const val DROPPED = 4
+        const val PLAN_TO_READ = 6
+
+        const val DEFAULT_STATUS = READING
+        const val DEFAULT_SCORE = 0
+
+        const val BASE_URL = "https://myanimelist.net"
+        const val USER_SESSION_COOKIE = "MALSESSIONID"
+        const val LOGGED_IN_COOKIE = "is_logged_in"
+    }
+
+    private val interceptor by lazy { MyAnimeListInterceptor(this) }
+    private val api by lazy { MyanimelistApi(client, interceptor) }
+
+    override val name: String
+        get() = "MyAnimeList"
+
+    override fun getLogo() = R.drawable.mal
+
+    override fun getLogoColor() = Color.rgb(46, 81, 162)
+
+    override fun getStatus(status: Int): String = with(context) {
+        when (status) {
+            READING -> getString(R.string.reading)
+            COMPLETED -> getString(R.string.completed)
+            ON_HOLD -> getString(R.string.on_hold)
+            DROPPED -> getString(R.string.dropped)
+            PLAN_TO_READ -> getString(R.string.plan_to_read)
+            else -> ""
+        }
+    }
+
+    override fun getStatusList(): List<Int> {
+        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
+    }
+
+    override fun getScoreList(): List<String> {
+        return IntRange(0, 10).map(Int::toString)
+    }
+
+    override fun displayScore(track: Track): String {
+        return track.score.toInt().toString()
+    }
+
+    override fun add(track: Track): Observable<Track> {
+        return api.addLibManga(track)
+    }
+
+    override fun update(track: Track): Observable<Track> {
+        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
+            track.status = COMPLETED
+        }
+
+        return api.updateLibManga(track)
+    }
+
+    override fun bind(track: Track): Observable<Track> {
+        return api.findLibManga(track)
+                .flatMap { remoteTrack ->
+                    if (remoteTrack != null) {
+                        track.copyPersonalFrom(remoteTrack)
+                        update(track)
+                    } else {
+                        // Set default fields if it's not found in the list
+                        track.score = DEFAULT_SCORE.toFloat()
+                        track.status = DEFAULT_STATUS
+                        add(track)
+                    }
+                }
+    }
+
+    override fun search(query: String): Observable<List<TrackSearch>> {
+        return api.search(query)
+    }
+
+    override fun refresh(track: Track): Observable<Track> {
+        return api.getLibManga(track)
+                .map { remoteTrack ->
+                    track.copyPersonalFrom(remoteTrack)
+                    track.total_chapters = remoteTrack.total_chapters
+                    track
+                }
+    }
+
+    override fun login(username: String, password: String): Completable {
+        logout()
+
+        return Observable.fromCallable { api.login(username, password) }
+                .doOnNext { csrf -> saveCSRF(csrf) }
+                .doOnNext { saveCredentials(username, password) }
+                .doOnError { logout() }
+                .toCompletable()
+    }
+
+    fun refreshLogin() {
+        val username = getUsername()
+        val password = getPassword()
+        logout()
+
+        try {
+            val csrf = api.login(username, password)
+            saveCSRF(csrf)
+            saveCredentials(username, password)
+        } catch (e: Exception) {
+            logout()
+            throw e
+        }
+    }
+
+    // Attempt to login again if cookies have been cleared but credentials are still filled
+    fun ensureLoggedIn() {
+        if (isAuthorized) return
+        if (!isLogged) throw Exception("MAL Login Credentials not found")
+
+        refreshLogin()
+    }
+
+    override fun logout() {
+        super.logout()
+        preferences.trackToken(this).delete()
+        networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
+    }
+
+    val isAuthorized: Boolean
+        get() = super.isLogged &&
+                getCSRF().isNotEmpty() &&
+                checkCookies()
+
+    fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
+
+    private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
+
+    private fun checkCookies(): Boolean {
+        var ckCount = 0
+        val url = HttpUrl.parse(BASE_URL)!!
+        for (ck in networkService.cookieManager.get(url)) {
+            if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
+                ckCount++
+        }
+
+        return ckCount == 2
+    }
+
+}

+ 13 - 13
app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt

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

+ 139 - 139
app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt

@@ -1,139 +1,139 @@
-package eu.kanade.tachiyomi.data.track.shikimori
-
-import android.content.Context
-import android.graphics.Color
-import android.util.Log
-import com.google.gson.Gson
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import rx.Completable
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-
-class Shikimori(private val context: Context, id: Int) : TrackService(id) {
-
-    override fun getScoreList(): List<String> {
-        return IntRange(0, 10).map(Int::toString)
-    }
-
-    override fun displayScore(track: Track): String {
-        return track.score.toInt().toString()
-    }
-
-    override fun add(track: Track): Observable<Track> {
-        return api.addLibManga(track, getUsername())
-    }
-
-    override fun update(track: Track): Observable<Track> {
-        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
-            track.status = COMPLETED
-        }
-        return api.updateLibManga(track, getUsername())
-    }
-
-    override fun bind(track: Track): Observable<Track> {
-        return api.findLibManga(track, getUsername())
-                .flatMap { remoteTrack ->
-                    if (remoteTrack != null) {
-                        track.copyPersonalFrom(remoteTrack)
-                        track.library_id = remoteTrack.library_id
-                        update(track)
-                    } else {
-                        // Set default fields if it's not found in the list
-                        track.score = DEFAULT_SCORE.toFloat()
-                        track.status = DEFAULT_STATUS
-                        add(track)
-                    }
-                }
-    }
-
-    override fun search(query: String): Observable<List<TrackSearch>> {
-        return api.search(query)
-    }
-
-    override fun refresh(track: Track): Observable<Track> {
-        return api.findLibManga(track, getUsername())
-                .map { remoteTrack ->
-                    if (remoteTrack != null) {
-                        track.copyPersonalFrom(remoteTrack)
-                        track.total_chapters = remoteTrack.total_chapters
-                    }
-                    track
-                }
-    }
-
-    companion object {
-        const val READING = 1
-        const val COMPLETED = 2
-        const val ON_HOLD = 3
-        const val DROPPED = 4
-        const val PLANNING = 5
-        const val REPEATING = 6
-
-        const val DEFAULT_STATUS = READING
-        const val DEFAULT_SCORE = 0
-    }
-
-    override val name = "Shikimori"
-
-    private val gson: Gson by injectLazy()
-
-    private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
-
-    private val api by lazy { ShikimoriApi(client, interceptor) }
-
-    override fun getLogo() = R.drawable.shikimori
-
-    override fun getLogoColor() = Color.rgb(40, 40, 40)
-
-    override fun getStatusList(): List<Int> {
-        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
-    }
-
-    override fun getStatus(status: Int): String = with(context) {
-        when (status) {
-            READING -> getString(R.string.reading)
-            COMPLETED -> getString(R.string.completed)
-            ON_HOLD -> getString(R.string.on_hold)
-            DROPPED -> getString(R.string.dropped)
-            PLANNING -> getString(R.string.plan_to_read)
-            REPEATING -> getString(R.string.repeating)
-            else -> ""
-        }
-    }
-
-    override fun login(username: String, password: String) = login(password)
-
-    fun login(code: String): Completable {
-        return api.accessToken(code).map { oauth: OAuth? ->
-            interceptor.newAuth(oauth)
-            if (oauth != null) {
-                val user = api.getCurrentUser()
-                saveCredentials(user.toString(), oauth.access_token)
-            }
-        }.doOnError {
-            logout()
-        }.toCompletable()
-    }
-
-    fun saveToken(oauth: OAuth?) {
-        val json = gson.toJson(oauth)
-        preferences.trackToken(this).set(json)
-    }
-
-    fun restoreToken(): OAuth? {
-        return try {
-            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
-        } catch (e: Exception) {
-            null
-        }
-    }
-
-    override fun logout() {
-        super.logout()
-        preferences.trackToken(this).set(null)
-        interceptor.newAuth(null)
-    }
-}
+package eu.kanade.tachiyomi.data.track.shikimori
+
+import android.content.Context
+import android.graphics.Color
+import android.util.Log
+import com.google.gson.Gson
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import rx.Completable
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+
+class Shikimori(private val context: Context, id: Int) : TrackService(id) {
+
+    override fun getScoreList(): List<String> {
+        return IntRange(0, 10).map(Int::toString)
+    }
+
+    override fun displayScore(track: Track): String {
+        return track.score.toInt().toString()
+    }
+
+    override fun add(track: Track): Observable<Track> {
+        return api.addLibManga(track, getUsername())
+    }
+
+    override fun update(track: Track): Observable<Track> {
+        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
+            track.status = COMPLETED
+        }
+        return api.updateLibManga(track, getUsername())
+    }
+
+    override fun bind(track: Track): Observable<Track> {
+        return api.findLibManga(track, getUsername())
+                .flatMap { remoteTrack ->
+                    if (remoteTrack != null) {
+                        track.copyPersonalFrom(remoteTrack)
+                        track.library_id = remoteTrack.library_id
+                        update(track)
+                    } else {
+                        // Set default fields if it's not found in the list
+                        track.score = DEFAULT_SCORE.toFloat()
+                        track.status = DEFAULT_STATUS
+                        add(track)
+                    }
+                }
+    }
+
+    override fun search(query: String): Observable<List<TrackSearch>> {
+        return api.search(query)
+    }
+
+    override fun refresh(track: Track): Observable<Track> {
+        return api.findLibManga(track, getUsername())
+                .map { remoteTrack ->
+                    if (remoteTrack != null) {
+                        track.copyPersonalFrom(remoteTrack)
+                        track.total_chapters = remoteTrack.total_chapters
+                    }
+                    track
+                }
+    }
+
+    companion object {
+        const val READING = 1
+        const val COMPLETED = 2
+        const val ON_HOLD = 3
+        const val DROPPED = 4
+        const val PLANNING = 5
+        const val REPEATING = 6
+
+        const val DEFAULT_STATUS = READING
+        const val DEFAULT_SCORE = 0
+    }
+
+    override val name = "Shikimori"
+
+    private val gson: Gson by injectLazy()
+
+    private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
+
+    private val api by lazy { ShikimoriApi(client, interceptor) }
+
+    override fun getLogo() = R.drawable.shikimori
+
+    override fun getLogoColor() = Color.rgb(40, 40, 40)
+
+    override fun getStatusList(): List<Int> {
+        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
+    }
+
+    override fun getStatus(status: Int): String = with(context) {
+        when (status) {
+            READING -> getString(R.string.reading)
+            COMPLETED -> getString(R.string.completed)
+            ON_HOLD -> getString(R.string.on_hold)
+            DROPPED -> getString(R.string.dropped)
+            PLANNING -> getString(R.string.plan_to_read)
+            REPEATING -> getString(R.string.repeating)
+            else -> ""
+        }
+    }
+
+    override fun login(username: String, password: String) = login(password)
+
+    fun login(code: String): Completable {
+        return api.accessToken(code).map { oauth: OAuth? ->
+            interceptor.newAuth(oauth)
+            if (oauth != null) {
+                val user = api.getCurrentUser()
+                saveCredentials(user.toString(), oauth.access_token)
+            }
+        }.doOnError {
+            logout()
+        }.toCompletable()
+    }
+
+    fun saveToken(oauth: OAuth?) {
+        val json = gson.toJson(oauth)
+        preferences.trackToken(this).set(json)
+    }
+
+    fun restoreToken(): OAuth? {
+        return try {
+            gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
+        } catch (e: Exception) {
+            null
+        }
+    }
+
+    override fun logout() {
+        super.logout()
+        preferences.trackToken(this).set(null)
+        interceptor.newAuth(null)
+    }
+}

+ 154 - 154
app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt

@@ -1,154 +1,154 @@
-package eu.kanade.tachiyomi.network
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.webkit.WebResourceResponse
-import android.webkit.WebSettings
-import android.webkit.WebView
-import eu.kanade.tachiyomi.util.WebViewClientCompat
-import okhttp3.Interceptor
-import okhttp3.Request
-import okhttp3.Response
-import java.io.IOException
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-
-class CloudflareInterceptor(private val context: Context) : Interceptor {
-
-    private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
-
-    private val handler = Handler(Looper.getMainLooper())
-
-    /**
-     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
-     * blocking the main thread too much. If used too often we could consider moving it to the
-     * Application class.
-     */
-    private val initWebView by lazy {
-        if (Build.VERSION.SDK_INT >= 17) {
-            WebSettings.getDefaultUserAgent(context)
-        } else {
-            null
-        }
-    }
-
-    @Synchronized
-    override fun intercept(chain: Interceptor.Chain): Response {
-        initWebView
-
-        val response = chain.proceed(chain.request())
-
-        // Check if Cloudflare anti-bot is on
-        if (response.code() == 503 && response.header("Server") in serverCheck) {
-            try {
-                response.close()
-                val solutionRequest = resolveWithWebView(chain.request())
-                return chain.proceed(solutionRequest)
-            } catch (e: Exception) {
-                // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
-                // we don't crash the entire app
-                throw IOException(e)
-            }
-        }
-
-        return response
-    }
-
-    private fun isChallengeSolutionUrl(url: String): Boolean {
-        return "chk_jschl" in url
-    }
-
-    @SuppressLint("SetJavaScriptEnabled")
-    private fun resolveWithWebView(request: Request): Request {
-        // We need to lock this thread until the WebView finds the challenge solution url, because
-        // OkHttp doesn't support asynchronous interceptors.
-        val latch = CountDownLatch(1)
-
-        var webView: WebView? = null
-        var solutionUrl: String? = null
-        var challengeFound = false
-
-        val origRequestUrl = request.url().toString()
-        val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
-
-        handler.post {
-            val view = WebView(context)
-            webView = view
-            view.settings.javaScriptEnabled = true
-            view.settings.userAgentString = request.header("User-Agent")
-            view.webViewClient = object : WebViewClientCompat() {
-
-                override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
-                    if (isChallengeSolutionUrl(url)) {
-                        solutionUrl = url
-                        latch.countDown()
-                    }
-                    return solutionUrl != null
-                }
-
-                override fun shouldInterceptRequestCompat(
-                        view: WebView,
-                        url: String
-                ): WebResourceResponse? {
-                    if (solutionUrl != null) {
-                        // Intercept any request when we have the solution.
-                        return WebResourceResponse("text/plain", "UTF-8", null)
-                    }
-                    return null
-                }
-
-                override fun onPageFinished(view: WebView, url: String) {
-                    // Http error codes are only received since M
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-                        url == origRequestUrl && !challengeFound
-                    ) {
-                        // The first request didn't return the challenge, abort.
-                        latch.countDown()
-                    }
-                }
-
-                override fun onReceivedErrorCompat(
-                        view: WebView,
-                        errorCode: Int,
-                        description: String?,
-                        failingUrl: String,
-                        isMainFrame: Boolean
-                ) {
-                    if (isMainFrame) {
-                        if (errorCode == 503) {
-                            // Found the cloudflare challenge page.
-                            challengeFound = true
-                        } else {
-                            // Unlock thread, the challenge wasn't found.
-                            latch.countDown()
-                        }
-                    }
-                }
-            }
-            webView?.loadUrl(origRequestUrl, headers)
-        }
-
-        // Wait a reasonable amount of time to retrieve the solution. The minimum should be
-        // around 4 seconds but it can take more due to slow networks or server issues.
-        latch.await(12, TimeUnit.SECONDS)
-
-        handler.post {
-            webView?.stopLoading()
-            webView?.destroy()
-        }
-
-        val solution = solutionUrl ?: throw Exception("Challenge not found")
-
-        return Request.Builder().get()
-            .url(solution)
-            .headers(request.headers())
-            .addHeader("Referer", origRequestUrl)
-            .addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
-            .addHeader("Accept-Language", "en")
-            .build()
-    }
-
-}
+package eu.kanade.tachiyomi.network
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.webkit.WebResourceResponse
+import android.webkit.WebSettings
+import android.webkit.WebView
+import eu.kanade.tachiyomi.util.WebViewClientCompat
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class CloudflareInterceptor(private val context: Context) : Interceptor {
+
+    private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
+
+    private val handler = Handler(Looper.getMainLooper())
+
+    /**
+     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
+     * blocking the main thread too much. If used too often we could consider moving it to the
+     * Application class.
+     */
+    private val initWebView by lazy {
+        if (Build.VERSION.SDK_INT >= 17) {
+            WebSettings.getDefaultUserAgent(context)
+        } else {
+            null
+        }
+    }
+
+    @Synchronized
+    override fun intercept(chain: Interceptor.Chain): Response {
+        initWebView
+
+        val response = chain.proceed(chain.request())
+
+        // Check if Cloudflare anti-bot is on
+        if (response.code() == 503 && response.header("Server") in serverCheck) {
+            try {
+                response.close()
+                val solutionRequest = resolveWithWebView(chain.request())
+                return chain.proceed(solutionRequest)
+            } catch (e: Exception) {
+                // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
+                // we don't crash the entire app
+                throw IOException(e)
+            }
+        }
+
+        return response
+    }
+
+    private fun isChallengeSolutionUrl(url: String): Boolean {
+        return "chk_jschl" in url
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private fun resolveWithWebView(request: Request): Request {
+        // We need to lock this thread until the WebView finds the challenge solution url, because
+        // OkHttp doesn't support asynchronous interceptors.
+        val latch = CountDownLatch(1)
+
+        var webView: WebView? = null
+        var solutionUrl: String? = null
+        var challengeFound = false
+
+        val origRequestUrl = request.url().toString()
+        val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
+
+        handler.post {
+            val view = WebView(context)
+            webView = view
+            view.settings.javaScriptEnabled = true
+            view.settings.userAgentString = request.header("User-Agent")
+            view.webViewClient = object : WebViewClientCompat() {
+
+                override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
+                    if (isChallengeSolutionUrl(url)) {
+                        solutionUrl = url
+                        latch.countDown()
+                    }
+                    return solutionUrl != null
+                }
+
+                override fun shouldInterceptRequestCompat(
+                        view: WebView,
+                        url: String
+                ): WebResourceResponse? {
+                    if (solutionUrl != null) {
+                        // Intercept any request when we have the solution.
+                        return WebResourceResponse("text/plain", "UTF-8", null)
+                    }
+                    return null
+                }
+
+                override fun onPageFinished(view: WebView, url: String) {
+                    // Http error codes are only received since M
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+                        url == origRequestUrl && !challengeFound
+                    ) {
+                        // The first request didn't return the challenge, abort.
+                        latch.countDown()
+                    }
+                }
+
+                override fun onReceivedErrorCompat(
+                        view: WebView,
+                        errorCode: Int,
+                        description: String?,
+                        failingUrl: String,
+                        isMainFrame: Boolean
+                ) {
+                    if (isMainFrame) {
+                        if (errorCode == 503) {
+                            // Found the cloudflare challenge page.
+                            challengeFound = true
+                        } else {
+                            // Unlock thread, the challenge wasn't found.
+                            latch.countDown()
+                        }
+                    }
+                }
+            }
+            webView?.loadUrl(origRequestUrl, headers)
+        }
+
+        // Wait a reasonable amount of time to retrieve the solution. The minimum should be
+        // around 4 seconds but it can take more due to slow networks or server issues.
+        latch.await(12, TimeUnit.SECONDS)
+
+        handler.post {
+            webView?.stopLoading()
+            webView?.destroy()
+        }
+
+        val solution = solutionUrl ?: throw Exception("Challenge not found")
+
+        return Request.Builder().get()
+            .url(solution)
+            .headers(request.headers())
+            .addHeader("Referer", origRequestUrl)
+            .addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
+            .addHeader("Accept-Language", "en")
+            .build()
+    }
+
+}

+ 117 - 117
app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt

@@ -1,117 +1,117 @@
-package eu.kanade.tachiyomi.network
-
-import android.content.Context
-import android.os.Build
-import okhttp3.*
-import java.io.File
-import java.io.IOException
-import java.net.InetAddress
-import java.net.Socket
-import java.net.UnknownHostException
-import java.security.KeyManagementException
-import java.security.KeyStore
-import java.security.NoSuchAlgorithmException
-import javax.net.ssl.*
-
-class NetworkHelper(context: Context) {
-
-    private val cacheDir = File(context.cacheDir, "network_cache")
-
-    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
-
-    val cookieManager = AndroidCookieJar(context)
-
-    val client = OkHttpClient.Builder()
-            .cookieJar(cookieManager)
-            .cache(Cache(cacheDir, cacheSize))
-            .enableTLS12()
-            .build()
-
-    val cloudflareClient = client.newBuilder()
-            .addInterceptor(CloudflareInterceptor(context))
-            .build()
-
-    private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
-            return this
-        }
-
-        val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
-        trustManagerFactory.init(null as KeyStore?)
-        val trustManagers = trustManagerFactory.trustManagers
-        if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
-            class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
-            constructor() : SSLSocketFactory() {
-
-                private val internalSSLSocketFactory: SSLSocketFactory
-
-                init {
-                    val context = SSLContext.getInstance("TLS")
-                    context.init(null, null, null)
-                    internalSSLSocketFactory = context.socketFactory
-                }
-
-                override fun getDefaultCipherSuites(): Array<String> {
-                    return internalSSLSocketFactory.defaultCipherSuites
-                }
-
-                override fun getSupportedCipherSuites(): Array<String> {
-                    return internalSSLSocketFactory.supportedCipherSuites
-                }
-
-                @Throws(IOException::class)
-                override fun createSocket(): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket())
-                }
-
-                @Throws(IOException::class)
-                override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
-                }
-
-                @Throws(IOException::class, UnknownHostException::class)
-                override fun createSocket(host: String, port: Int): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
-                }
-
-                @Throws(IOException::class, UnknownHostException::class)
-                override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
-                }
-
-                @Throws(IOException::class)
-                override fun createSocket(host: InetAddress, port: Int): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
-                }
-
-                @Throws(IOException::class)
-                override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? {
-                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
-                }
-
-                private fun enableTLSOnSocket(socket: Socket?): Socket? {
-                    if (socket != null && socket is SSLSocket) {
-                        socket.enabledProtocols = socket.supportedProtocols
-                    }
-                    return socket
-                }
-            }
-
-            sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager)
-        }
-
-        val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
-            .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
-            .cipherSuites(
-                    *ConnectionSpec.MODERN_TLS.cipherSuites().orEmpty().toTypedArray(),
-                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
-                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
-            )
-            .build()
-
-        val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT)
-        connectionSpecs(specs)
-
-        return this
-    }
-}
+package eu.kanade.tachiyomi.network
+
+import android.content.Context
+import android.os.Build
+import okhttp3.*
+import java.io.File
+import java.io.IOException
+import java.net.InetAddress
+import java.net.Socket
+import java.net.UnknownHostException
+import java.security.KeyManagementException
+import java.security.KeyStore
+import java.security.NoSuchAlgorithmException
+import javax.net.ssl.*
+
+class NetworkHelper(context: Context) {
+
+    private val cacheDir = File(context.cacheDir, "network_cache")
+
+    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
+
+    val cookieManager = AndroidCookieJar(context)
+
+    val client = OkHttpClient.Builder()
+            .cookieJar(cookieManager)
+            .cache(Cache(cacheDir, cacheSize))
+            .enableTLS12()
+            .build()
+
+    val cloudflareClient = client.newBuilder()
+            .addInterceptor(CloudflareInterceptor(context))
+            .build()
+
+    private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+            return this
+        }
+
+        val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
+        trustManagerFactory.init(null as KeyStore?)
+        val trustManagers = trustManagerFactory.trustManagers
+        if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
+            class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
+            constructor() : SSLSocketFactory() {
+
+                private val internalSSLSocketFactory: SSLSocketFactory
+
+                init {
+                    val context = SSLContext.getInstance("TLS")
+                    context.init(null, null, null)
+                    internalSSLSocketFactory = context.socketFactory
+                }
+
+                override fun getDefaultCipherSuites(): Array<String> {
+                    return internalSSLSocketFactory.defaultCipherSuites
+                }
+
+                override fun getSupportedCipherSuites(): Array<String> {
+                    return internalSSLSocketFactory.supportedCipherSuites
+                }
+
+                @Throws(IOException::class)
+                override fun createSocket(): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket())
+                }
+
+                @Throws(IOException::class)
+                override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
+                }
+
+                @Throws(IOException::class, UnknownHostException::class)
+                override fun createSocket(host: String, port: Int): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
+                }
+
+                @Throws(IOException::class, UnknownHostException::class)
+                override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
+                }
+
+                @Throws(IOException::class)
+                override fun createSocket(host: InetAddress, port: Int): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
+                }
+
+                @Throws(IOException::class)
+                override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? {
+                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
+                }
+
+                private fun enableTLSOnSocket(socket: Socket?): Socket? {
+                    if (socket != null && socket is SSLSocket) {
+                        socket.enabledProtocols = socket.supportedProtocols
+                    }
+                    return socket
+                }
+            }
+
+            sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager)
+        }
+
+        val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+            .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
+            .cipherSuites(
+                    *ConnectionSpec.MODERN_TLS.cipherSuites().orEmpty().toTypedArray(),
+                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
+            )
+            .build()
+
+        val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT)
+        connectionSpecs(specs)
+
+        return this
+    }
+}

+ 69 - 69
app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt

@@ -1,70 +1,70 @@
-package eu.kanade.tachiyomi.network
-
-import okhttp3.Call
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import rx.Observable
-import rx.Producer
-import rx.Subscription
-import java.util.concurrent.atomic.AtomicBoolean
-
-fun Call.asObservable(): Observable<Response> {
-    return Observable.unsafeCreate { subscriber ->
-        // Since Call is a one-shot type, clone it for each new subscriber.
-        val call = clone()
-
-        // Wrap the call in a helper which handles both unsubscription and backpressure.
-        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
-            override fun request(n: Long) {
-                if (n == 0L || !compareAndSet(false, true)) return
-
-                try {
-                    val response = call.execute()
-                    if (!subscriber.isUnsubscribed) {
-                        subscriber.onNext(response)
-                        subscriber.onCompleted()
-                    }
-                } catch (error: Exception) {
-                    if (!subscriber.isUnsubscribed) {
-                        subscriber.onError(error)
-                    }
-                }
-            }
-
-            override fun unsubscribe() {
-                call.cancel()
-            }
-
-            override fun isUnsubscribed(): Boolean {
-                return call.isCanceled
-            }
-        }
-
-        subscriber.add(requestArbiter)
-        subscriber.setProducer(requestArbiter)
-    }
-}
-
-fun Call.asObservableSuccess(): Observable<Response> {
-    return asObservable().doOnNext { response ->
-        if (!response.isSuccessful) {
-            response.close()
-            throw Exception("HTTP error ${response.code()}")
-        }
-    }
-}
-
-fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
-    val progressClient = newBuilder()
-            .cache(null)
-            .addNetworkInterceptor { chain ->
-                val originalResponse = chain.proceed(chain.request())
-                originalResponse.newBuilder()
-                        .body(ProgressResponseBody(originalResponse.body()!!, listener))
-                        .build()
-            }
-            .build()
-
-    return progressClient.newCall(request)
+package eu.kanade.tachiyomi.network
+
+import okhttp3.Call
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import rx.Producer
+import rx.Subscription
+import java.util.concurrent.atomic.AtomicBoolean
+
+fun Call.asObservable(): Observable<Response> {
+    return Observable.unsafeCreate { subscriber ->
+        // Since Call is a one-shot type, clone it for each new subscriber.
+        val call = clone()
+
+        // Wrap the call in a helper which handles both unsubscription and backpressure.
+        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
+            override fun request(n: Long) {
+                if (n == 0L || !compareAndSet(false, true)) return
+
+                try {
+                    val response = call.execute()
+                    if (!subscriber.isUnsubscribed) {
+                        subscriber.onNext(response)
+                        subscriber.onCompleted()
+                    }
+                } catch (error: Exception) {
+                    if (!subscriber.isUnsubscribed) {
+                        subscriber.onError(error)
+                    }
+                }
+            }
+
+            override fun unsubscribe() {
+                call.cancel()
+            }
+
+            override fun isUnsubscribed(): Boolean {
+                return call.isCanceled
+            }
+        }
+
+        subscriber.add(requestArbiter)
+        subscriber.setProducer(requestArbiter)
+    }
+}
+
+fun Call.asObservableSuccess(): Observable<Response> {
+    return asObservable().doOnNext { response ->
+        if (!response.isSuccessful) {
+            response.close()
+            throw Exception("HTTP error ${response.code()}")
+        }
+    }
+}
+
+fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
+    val progressClient = newBuilder()
+            .cache(null)
+            .addNetworkInterceptor { chain ->
+                val originalResponse = chain.proceed(chain.request())
+                originalResponse.newBuilder()
+                        .body(ProgressResponseBody(originalResponse.body()!!, listener))
+                        .build()
+            }
+            .build()
+
+    return progressClient.newCall(request)
 }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt

@@ -1,5 +1,5 @@
-package eu.kanade.tachiyomi.network
-
-interface ProgressListener {
-    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
+package eu.kanade.tachiyomi.network
+
+interface ProgressListener {
+    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
 }

+ 39 - 39
app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt

@@ -1,40 +1,40 @@
-package eu.kanade.tachiyomi.network
-
-import okhttp3.MediaType
-import okhttp3.ResponseBody
-import okio.*
-import java.io.IOException
-
-class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
-
-    private val bufferedSource: BufferedSource by lazy {
-        Okio.buffer(source(responseBody.source()))
-    }
-
-    override fun contentType(): MediaType {
-        return responseBody.contentType()!!
-    }
-
-    override fun contentLength(): Long {
-        return responseBody.contentLength()
-    }
-
-    override fun source(): BufferedSource {
-        return bufferedSource
-    }
-
-    private fun source(source: Source): Source {
-        return object : ForwardingSource(source) {
-            internal var totalBytesRead = 0L
-
-            @Throws(IOException::class)
-            override fun read(sink: Buffer, byteCount: Long): Long {
-                val bytesRead = super.read(sink, byteCount)
-                // read() returns the number of bytes read, or -1 if this source is exhausted.
-                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
-                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
-                return bytesRead
-            }
-        }
-    }
+package eu.kanade.tachiyomi.network
+
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import okio.*
+import java.io.IOException
+
+class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
+
+    private val bufferedSource: BufferedSource by lazy {
+        Okio.buffer(source(responseBody.source()))
+    }
+
+    override fun contentType(): MediaType {
+        return responseBody.contentType()!!
+    }
+
+    override fun contentLength(): Long {
+        return responseBody.contentLength()
+    }
+
+    override fun source(): BufferedSource {
+        return bufferedSource
+    }
+
+    private fun source(source: Source): Source {
+        return object : ForwardingSource(source) {
+            internal var totalBytesRead = 0L
+
+            @Throws(IOException::class)
+            override fun read(sink: Buffer, byteCount: Long): Long {
+                val bytesRead = super.read(sink, byteCount)
+                // read() returns the number of bytes read, or -1 if this source is exhausted.
+                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
+                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
+                return bytesRead
+            }
+        }
+    }
 }

+ 32 - 32
app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt

@@ -1,32 +1,32 @@
-package eu.kanade.tachiyomi.network
-
-import okhttp3.*
-import java.util.concurrent.TimeUnit.MINUTES
-
-private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
-private val DEFAULT_HEADERS = Headers.Builder().build()
-private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
-
-fun GET(url: String,
-        headers: Headers = DEFAULT_HEADERS,
-        cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
-
-    return Request.Builder()
-            .url(url)
-            .headers(headers)
-            .cacheControl(cache)
-            .build()
-}
-
-fun POST(url: String,
-         headers: Headers = DEFAULT_HEADERS,
-         body: RequestBody = DEFAULT_BODY,
-         cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
-
-    return Request.Builder()
-            .url(url)
-            .post(body)
-            .headers(headers)
-            .cacheControl(cache)
-            .build()
-}
+package eu.kanade.tachiyomi.network
+
+import okhttp3.*
+import java.util.concurrent.TimeUnit.MINUTES
+
+private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
+private val DEFAULT_HEADERS = Headers.Builder().build()
+private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
+
+fun GET(url: String,
+        headers: Headers = DEFAULT_HEADERS,
+        cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+    return Request.Builder()
+            .url(url)
+            .headers(headers)
+            .cacheControl(cache)
+            .build()
+}
+
+fun POST(url: String,
+         headers: Headers = DEFAULT_HEADERS,
+         body: RequestBody = DEFAULT_BODY,
+         cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+    return Request.Builder()
+            .url(url)
+            .post(body)
+            .headers(headers)
+            .cacheControl(cache)
+            .build()
+}

+ 45 - 45
app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt

@@ -1,46 +1,46 @@
-package eu.kanade.tachiyomi.source
-
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.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
+package eu.kanade.tachiyomi.source
+
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.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
 }

+ 43 - 43
app/src/main/java/eu/kanade/tachiyomi/source/Source.kt

@@ -1,44 +1,44 @@
-package eu.kanade.tachiyomi.source
-
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import rx.Observable
-
-/**
- * A basic interface for creating a source. It could be an online source, a local source, etc...
- */
-interface Source {
-
-    /**
-     * Id for the source. Must be unique.
-     */
-    val id: Long
-
-    /**
-     * Name of the source.
-     */
-    val name: String
-
-    /**
-     * Returns an observable with the updated details for a manga.
-     *
-     * @param manga the manga to update.
-     */
-    fun fetchMangaDetails(manga: SManga): Observable<SManga>
-
-    /**
-     * Returns an observable with all the available chapters for a manga.
-     *
-     * @param manga the manga to update.
-     */
-    fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
-
-    /**
-     * Returns an observable with the list of pages a chapter has.
-     *
-     * @param chapter the chapter.
-     */
-    fun fetchPageList(chapter: SChapter): Observable<List<Page>>
-
+package eu.kanade.tachiyomi.source
+
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import rx.Observable
+
+/**
+ * A basic interface for creating a source. It could be an online source, a local source, etc...
+ */
+interface Source {
+
+    /**
+     * Id for the source. Must be unique.
+     */
+    val id: Long
+
+    /**
+     * Name of the source.
+     */
+    val name: String
+
+    /**
+     * Returns an observable with the updated details for a manga.
+     *
+     * @param manga the manga to update.
+     */
+    fun fetchMangaDetails(manga: SManga): Observable<SManga>
+
+    /**
+     * Returns an observable with all the available chapters for a manga.
+     *
+     * @param manga the manga to update.
+     */
+    fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
+
+    /**
+     * Returns an observable with the list of pages a chapter has.
+     *
+     * @param chapter the chapter.
+     */
+    fun fetchPageList(chapter: SChapter): Observable<List<Page>>
+
 }

+ 74 - 74
app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt

@@ -1,74 +1,74 @@
-package eu.kanade.tachiyomi.source
-
-import android.content.Context
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import rx.Observable
-
-open class SourceManager(private val context: Context) {
-
-    private val sourcesMap = mutableMapOf<Long, Source>()
-
-    private val stubSourcesMap = mutableMapOf<Long, StubSource>()
-
-    init {
-        createInternalSources().forEach { registerSource(it) }
-    }
-
-    open fun get(sourceKey: Long): Source? {
-        return sourcesMap[sourceKey]
-    }
-
-    fun getOrStub(sourceKey: Long): Source {
-        return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
-            StubSource(sourceKey)
-        }
-    }
-
-    fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
-
-    fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
-
-    internal fun registerSource(source: Source, overwrite: Boolean = false) {
-        if (overwrite || !sourcesMap.containsKey(source.id)) {
-            sourcesMap[source.id] = source
-        }
-    }
-
-    internal fun unregisterSource(source: Source) {
-        sourcesMap.remove(source.id)
-    }
-
-    private fun createInternalSources(): List<Source> = listOf(
-            LocalSource(context)
-    )
-
-    private inner class StubSource(override val id: Long) : Source {
-
-        override val name: String
-            get() = id.toString()
-
-        override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
-            return Observable.error(getSourceNotInstalledException())
-        }
-
-        override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
-            return Observable.error(getSourceNotInstalledException())
-        }
-
-        override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
-            return Observable.error(getSourceNotInstalledException())
-        }
-
-        override fun toString(): String {
-            return name
-        }
-
-        private fun getSourceNotInstalledException(): Exception {
-            return Exception(context.getString(R.string.source_not_installed, id.toString()))
-        }
-    }
-}
+package eu.kanade.tachiyomi.source
+
+import android.content.Context
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import rx.Observable
+
+open class SourceManager(private val context: Context) {
+
+    private val sourcesMap = mutableMapOf<Long, Source>()
+
+    private val stubSourcesMap = mutableMapOf<Long, StubSource>()
+
+    init {
+        createInternalSources().forEach { registerSource(it) }
+    }
+
+    open fun get(sourceKey: Long): Source? {
+        return sourcesMap[sourceKey]
+    }
+
+    fun getOrStub(sourceKey: Long): Source {
+        return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
+            StubSource(sourceKey)
+        }
+    }
+
+    fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
+
+    fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
+
+    internal fun registerSource(source: Source, overwrite: Boolean = false) {
+        if (overwrite || !sourcesMap.containsKey(source.id)) {
+            sourcesMap[source.id] = source
+        }
+    }
+
+    internal fun unregisterSource(source: Source) {
+        sourcesMap.remove(source.id)
+    }
+
+    private fun createInternalSources(): List<Source> = listOf(
+            LocalSource(context)
+    )
+
+    private inner class StubSource(override val id: Long) : Source {
+
+        override val name: String
+            get() = id.toString()
+
+        override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+            return Observable.error(getSourceNotInstalledException())
+        }
+
+        override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+            return Observable.error(getSourceNotInstalledException())
+        }
+
+        override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
+            return Observable.error(getSourceNotInstalledException())
+        }
+
+        override fun toString(): String {
+            return name
+        }
+
+        private fun getSourceNotInstalledException(): Exception {
+            return Exception(context.getString(R.string.source_not_installed, id.toString()))
+        }
+    }
+}

+ 39 - 39
app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt

@@ -1,40 +1,40 @@
-package eu.kanade.tachiyomi.source.model
-
-sealed class Filter<T>(val name: String, var state: T) {
-    open class Header(name: String) : Filter<Any>(name, 0)
-    open class Separator(name: String = "") : Filter<Any>(name, 0)
-    abstract class Select<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
-        }
-    }
-    abstract class Group<V>(name: String, state: List<V>): Filter<List<V>>(name, state)
-
-    abstract class Sort(name: String, val values: Array<String>, state: Selection? = null)
-        : Filter<Sort.Selection?>(name, state) {
-        data class Selection(val index: Int, val ascending: Boolean)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is Filter<*>) return false
-
-        return name == other.name && state == other.state
-    }
-
-    override fun hashCode(): Int {
-        var result = name.hashCode()
-        result = 31 * result + (state?.hashCode() ?: 0)
-        return result
-    }
-
+package eu.kanade.tachiyomi.source.model
+
+sealed class Filter<T>(val name: String, var state: T) {
+    open class Header(name: String) : Filter<Any>(name, 0)
+    open class Separator(name: String = "") : Filter<Any>(name, 0)
+    abstract class Select<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
+        }
+    }
+    abstract class Group<V>(name: String, state: List<V>): Filter<List<V>>(name, state)
+
+    abstract class Sort(name: String, val values: Array<String>, state: Selection? = null)
+        : Filter<Sort.Selection?>(name, state) {
+        data class Selection(val index: Int, val ascending: Boolean)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Filter<*>) return false
+
+        return name == other.name && state == other.state
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + (state?.hashCode() ?: 0)
+        return result
+    }
+
 }

+ 6 - 6
app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt

@@ -1,7 +1,7 @@
-package eu.kanade.tachiyomi.source.model
-
-data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
-
-    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
-
+package eu.kanade.tachiyomi.source.model
+
+data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
+
+    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
+
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt

@@ -1,3 +1,3 @@
-package eu.kanade.tachiyomi.source.model
-
+package eu.kanade.tachiyomi.source.model
+
 data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

+ 48 - 48
app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt

@@ -1,48 +1,48 @@
-package eu.kanade.tachiyomi.source.model
-
-import android.net.Uri
-import eu.kanade.tachiyomi.network.ProgressListener
-import rx.subjects.Subject
-
-open class Page(
-        val index: Int,
-        val url: String = "",
-        var imageUrl: String? = null,
-        @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
-) : ProgressListener {
-
-    val number: Int
-        get() = index + 1
-
-    @Transient @Volatile var status: Int = 0
-        set(value) {
-            field = value
-            statusSubject?.onNext(value)
-        }
-
-    @Transient @Volatile var progress: Int = 0
-
-    @Transient private var statusSubject: Subject<Int, Int>? = null
-
-    override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
-        progress = if (contentLength > 0) {
-            (100 * bytesRead / contentLength).toInt()
-        } else {
-            -1
-        }
-    }
-
-    fun setStatusSubject(subject: Subject<Int, Int>?) {
-        this.statusSubject = subject
-    }
-
-    companion object {
-
-        const val QUEUE = 0
-        const val LOAD_PAGE = 1
-        const val DOWNLOAD_IMAGE = 2
-        const val READY = 3
-        const val ERROR = 4
-    }
-
-}
+package eu.kanade.tachiyomi.source.model
+
+import android.net.Uri
+import eu.kanade.tachiyomi.network.ProgressListener
+import rx.subjects.Subject
+
+open class Page(
+        val index: Int,
+        val url: String = "",
+        var imageUrl: String? = null,
+        @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
+) : ProgressListener {
+
+    val number: Int
+        get() = index + 1
+
+    @Transient @Volatile var status: Int = 0
+        set(value) {
+            field = value
+            statusSubject?.onNext(value)
+        }
+
+    @Transient @Volatile var progress: Int = 0
+
+    @Transient private var statusSubject: Subject<Int, Int>? = null
+
+    override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
+        progress = if (contentLength > 0) {
+            (100 * bytesRead / contentLength).toInt()
+        } else {
+            -1
+        }
+    }
+
+    fun setStatusSubject(subject: Subject<Int, Int>?) {
+        this.statusSubject = subject
+    }
+
+    companion object {
+
+        const val QUEUE = 0
+        const val LOAD_PAGE = 1
+        const val DOWNLOAD_IMAGE = 2
+        const val READY = 3
+        const val ERROR = 4
+    }
+
+}

+ 30 - 30
app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt

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

+ 14 - 14
app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt

@@ -1,15 +1,15 @@
-package eu.kanade.tachiyomi.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
-
-    override  var scanlator: String? = null
-
+package eu.kanade.tachiyomi.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
+
+    override  var scanlator: String? = null
+
 }

+ 57 - 57
app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt

@@ -1,58 +1,58 @@
-package eu.kanade.tachiyomi.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()
-        }
-    }
-
+package eu.kanade.tachiyomi.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()
+        }
+    }
+
 }

+ 22 - 22
app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt

@@ -1,23 +1,23 @@
-package eu.kanade.tachiyomi.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
-
+package eu.kanade.tachiyomi.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
+
 }

+ 367 - 367
app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt

@@ -1,367 +1,367 @@
-package eu.kanade.tachiyomi.source.online
-
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.network.asObservableSuccess
-import eu.kanade.tachiyomi.network.newCallWithProgress
-import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.model.*
-import okhttp3.Headers
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-import java.lang.Exception
-import java.net.URI
-import java.net.URISyntaxException
-import java.security.MessageDigest
-
-/**
- * A simple implementation for sources from a website.
- */
-abstract class HttpSource : CatalogueSource {
-
-    /**
-     * Network service.
-     */
-    protected val network: NetworkHelper by injectLazy()
-
-//    /**
-//     * Preferences that a source may need.
-//     */
-//    val preferences: SharedPreferences by lazy {
-//        Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
-//    }
-
-    /**
-     * Base url of the website without the trailing slash, like: http://mysite.com
-     */
-    abstract val baseUrl: String
-
-    /**
-     * 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.
-     */
-    open val versionId = 1
-
-    /**
-     * 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.
-     */
-    override val id by lazy {
-        val key = "${name.toLowerCase()}/$lang/$versionId"
-        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
-        (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
-    }
-
-    /**
-     * Headers used for requests.
-     */
-    val headers: Headers by lazy { headersBuilder().build() }
-
-    /**
-     * Default network client for doing requests.
-     */
-    open val client: OkHttpClient
-        get() = network.client
-
-    /**
-     * Headers builder for requests. Implementations can override this method for custom headers.
-     */
-    open protected fun headersBuilder() = Headers.Builder().apply {
-        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
-    }
-
-    /**
-     * Visible name of the source.
-     */
-    override fun toString() = "$name (${lang.toUpperCase()})"
-
-    /**
-     * Returns an observable containing a page with a list of manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param page the page number to retrieve.
-     */
-    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
-        return client.newCall(popularMangaRequest(page))
-                .asObservableSuccess()
-                .map { response ->
-                    popularMangaParse(response)
-                }
-    }
-
-    /**
-     * Returns the request for the popular manga given the page.
-     *
-     * @param page the page number to retrieve.
-     */
-    abstract protected fun popularMangaRequest(page: Int): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun popularMangaParse(response: Response): MangasPage
-
-    /**
-     * Returns an observable containing a page with a list of manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param page the page number to retrieve.
-     * @param query the search query.
-     * @param filters the list of filters to apply.
-     */
-    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 request for the search manga given the page.
-     *
-     * @param page the page number to retrieve.
-     * @param query the search query.
-     * @param filters the list of filters to apply.
-     */
-    abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun searchMangaParse(response: Response): MangasPage
-
-    /**
-     * Returns an observable containing a page with a list of latest manga updates.
-     *
-     * @param page the page number to retrieve.
-     */
-    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
-        return client.newCall(latestUpdatesRequest(page))
-                .asObservableSuccess()
-                .map { response ->
-                    latestUpdatesParse(response)
-                }
-    }
-
-    /**
-     * Returns the request for latest manga given the page.
-     *
-     * @param page the page number to retrieve.
-     */
-    abstract protected fun latestUpdatesRequest(page: Int): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun latestUpdatesParse(response: Response): MangasPage
-
-    /**
-     * Returns an observable with the updated details for a manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param manga the manga to be updated.
-     */
-    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
-        return client.newCall(mangaDetailsRequest(manga))
-                .asObservableSuccess()
-                .map { response ->
-                    mangaDetailsParse(response).apply { initialized = true }
-                }
-    }
-
-    /**
-     * Returns the request for the details of a manga. Override only if it's needed to change the
-     * url, send different headers or request method like POST.
-     *
-     * @param manga the manga to be updated.
-     */
-    open fun mangaDetailsRequest(manga: SManga): Request {
-        return GET(baseUrl + manga.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns the details of a manga.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun mangaDetailsParse(response: Response): SManga
-
-    /**
-     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
-     * override this method.  If a manga is licensed an empty chapter list observable is returned
-     *
-     * @param manga the manga to look for chapters.
-     */
-    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
-        if (manga.status != SManga.LICENSED) {
-            return client.newCall(chapterListRequest(manga))
-                    .asObservableSuccess()
-                    .map { response ->
-                        chapterListParse(response)
-                    }
-        } else {
-            return Observable.error(Exception("Licensed - No chapters to show"))
-        }
-    }
-
-    /**
-     * Returns the request for updating the chapter list. Override only if it's needed to override
-     * the url, send different headers or request method like POST.
-     *
-     * @param manga the manga to look for chapters.
-     */
-    open protected fun chapterListRequest(manga: SManga): Request {
-        return GET(baseUrl + manga.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns a list of chapters.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun chapterListParse(response: Response): List<SChapter>
-
-    /**
-     * Returns an observable with the page list for a chapter.
-     *
-     * @param chapter the chapter whose page list has to be fetched.
-     */
-    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
-        return client.newCall(pageListRequest(chapter))
-                .asObservableSuccess()
-                .map { response ->
-                    pageListParse(response)
-                }
-    }
-
-    /**
-     * Returns the request for getting the page list. Override only if it's needed to override the
-     * url, send different headers or request method like POST.
-     *
-     * @param chapter the chapter whose page list has to be fetched.
-     */
-    open protected fun pageListRequest(chapter: SChapter): Request {
-        return GET(baseUrl + chapter.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns a list of pages.
-     *
-     * @param response the response from the site.
-     */
-    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
-     * error, it will return null instead of throwing an exception.
-     *
-     * @param page the page whose source image has to be fetched.
-     */
-    open fun fetchImageUrl(page: Page): Observable<String> {
-        return client.newCall(imageUrlRequest(page))
-                .asObservableSuccess()
-                .map { imageUrlParse(it) }
-    }
-
-    /**
-     * Returns the request for getting the url to the source image. Override only if it's needed to
-     * override the url, send different headers or request method like POST.
-     *
-     * @param page the chapter whose page list has to be fetched
-     */
-    open protected fun imageUrlRequest(page: Page): Request {
-        return GET(page.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns the absolute url to the source image.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun imageUrlParse(response: Response): String
-
-    /**
-     * Returns an observable with the response of the source image.
-     *
-     * @param page the page whose source image has to be downloaded.
-     */
-    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
-     * the url, send different headers or request method like POST.
-     *
-     * @param page the chapter whose page list has to be fetched
-     */
-    open protected fun imageRequest(page: Page): Request {
-        return GET(page.imageUrl!!, headers)
-    }
-
-    /**
-     * 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 url the full url to the chapter.
-     */
-    fun SChapter.setUrlWithoutDomain(url: String) {
-        this.url = getUrlWithoutDomain(url)
-    }
-
-    /**
-     * 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 url the full url to the manga.
-     */
-    fun SManga.setUrlWithoutDomain(url: String) {
-        this.url = getUrlWithoutDomain(url)
-    }
-
-    /**
-     * 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
-        }
-    }
-
-    /**
-     * 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].
-     *
-     * @param chapter the chapter to be added.
-     * @param manga the manga of the chapter.
-     */
-    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
-    }
-
-    /**
-     * Returns the list of filters for the source.
-     */
-    override fun getFilterList() = FilterList()
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.network.newCallWithProgress
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.model.*
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.lang.Exception
+import java.net.URI
+import java.net.URISyntaxException
+import java.security.MessageDigest
+
+/**
+ * A simple implementation for sources from a website.
+ */
+abstract class HttpSource : CatalogueSource {
+
+    /**
+     * Network service.
+     */
+    protected val network: NetworkHelper by injectLazy()
+
+//    /**
+//     * Preferences that a source may need.
+//     */
+//    val preferences: SharedPreferences by lazy {
+//        Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
+//    }
+
+    /**
+     * Base url of the website without the trailing slash, like: http://mysite.com
+     */
+    abstract val baseUrl: String
+
+    /**
+     * 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.
+     */
+    open val versionId = 1
+
+    /**
+     * 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.
+     */
+    override val id by lazy {
+        val key = "${name.toLowerCase()}/$lang/$versionId"
+        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
+        (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
+    }
+
+    /**
+     * Headers used for requests.
+     */
+    val headers: Headers by lazy { headersBuilder().build() }
+
+    /**
+     * Default network client for doing requests.
+     */
+    open val client: OkHttpClient
+        get() = network.client
+
+    /**
+     * Headers builder for requests. Implementations can override this method for custom headers.
+     */
+    open protected fun headersBuilder() = Headers.Builder().apply {
+        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
+    }
+
+    /**
+     * Visible name of the source.
+     */
+    override fun toString() = "$name (${lang.toUpperCase()})"
+
+    /**
+     * Returns an observable containing a page with a list of manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param page the page number to retrieve.
+     */
+    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
+        return client.newCall(popularMangaRequest(page))
+                .asObservableSuccess()
+                .map { response ->
+                    popularMangaParse(response)
+                }
+    }
+
+    /**
+     * Returns the request for the popular manga given the page.
+     *
+     * @param page the page number to retrieve.
+     */
+    abstract protected fun popularMangaRequest(page: Int): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun popularMangaParse(response: Response): MangasPage
+
+    /**
+     * Returns an observable containing a page with a list of manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param page the page number to retrieve.
+     * @param query the search query.
+     * @param filters the list of filters to apply.
+     */
+    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 request for the search manga given the page.
+     *
+     * @param page the page number to retrieve.
+     * @param query the search query.
+     * @param filters the list of filters to apply.
+     */
+    abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun searchMangaParse(response: Response): MangasPage
+
+    /**
+     * Returns an observable containing a page with a list of latest manga updates.
+     *
+     * @param page the page number to retrieve.
+     */
+    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
+        return client.newCall(latestUpdatesRequest(page))
+                .asObservableSuccess()
+                .map { response ->
+                    latestUpdatesParse(response)
+                }
+    }
+
+    /**
+     * Returns the request for latest manga given the page.
+     *
+     * @param page the page number to retrieve.
+     */
+    abstract protected fun latestUpdatesRequest(page: Int): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun latestUpdatesParse(response: Response): MangasPage
+
+    /**
+     * Returns an observable with the updated details for a manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param manga the manga to be updated.
+     */
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        return client.newCall(mangaDetailsRequest(manga))
+                .asObservableSuccess()
+                .map { response ->
+                    mangaDetailsParse(response).apply { initialized = true }
+                }
+    }
+
+    /**
+     * Returns the request for the details of a manga. Override only if it's needed to change the
+     * url, send different headers or request method like POST.
+     *
+     * @param manga the manga to be updated.
+     */
+    open fun mangaDetailsRequest(manga: SManga): Request {
+        return GET(baseUrl + manga.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns the details of a manga.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun mangaDetailsParse(response: Response): SManga
+
+    /**
+     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
+     * override this method.  If a manga is licensed an empty chapter list observable is returned
+     *
+     * @param manga the manga to look for chapters.
+     */
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+        if (manga.status != SManga.LICENSED) {
+            return client.newCall(chapterListRequest(manga))
+                    .asObservableSuccess()
+                    .map { response ->
+                        chapterListParse(response)
+                    }
+        } else {
+            return Observable.error(Exception("Licensed - No chapters to show"))
+        }
+    }
+
+    /**
+     * Returns the request for updating the chapter list. Override only if it's needed to override
+     * the url, send different headers or request method like POST.
+     *
+     * @param manga the manga to look for chapters.
+     */
+    open protected fun chapterListRequest(manga: SManga): Request {
+        return GET(baseUrl + manga.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns a list of chapters.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun chapterListParse(response: Response): List<SChapter>
+
+    /**
+     * Returns an observable with the page list for a chapter.
+     *
+     * @param chapter the chapter whose page list has to be fetched.
+     */
+    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
+        return client.newCall(pageListRequest(chapter))
+                .asObservableSuccess()
+                .map { response ->
+                    pageListParse(response)
+                }
+    }
+
+    /**
+     * Returns the request for getting the page list. Override only if it's needed to override the
+     * url, send different headers or request method like POST.
+     *
+     * @param chapter the chapter whose page list has to be fetched.
+     */
+    open protected fun pageListRequest(chapter: SChapter): Request {
+        return GET(baseUrl + chapter.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns a list of pages.
+     *
+     * @param response the response from the site.
+     */
+    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
+     * error, it will return null instead of throwing an exception.
+     *
+     * @param page the page whose source image has to be fetched.
+     */
+    open fun fetchImageUrl(page: Page): Observable<String> {
+        return client.newCall(imageUrlRequest(page))
+                .asObservableSuccess()
+                .map { imageUrlParse(it) }
+    }
+
+    /**
+     * Returns the request for getting the url to the source image. Override only if it's needed to
+     * override the url, send different headers or request method like POST.
+     *
+     * @param page the chapter whose page list has to be fetched
+     */
+    open protected fun imageUrlRequest(page: Page): Request {
+        return GET(page.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns the absolute url to the source image.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun imageUrlParse(response: Response): String
+
+    /**
+     * Returns an observable with the response of the source image.
+     *
+     * @param page the page whose source image has to be downloaded.
+     */
+    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
+     * the url, send different headers or request method like POST.
+     *
+     * @param page the chapter whose page list has to be fetched
+     */
+    open protected fun imageRequest(page: Page): Request {
+        return GET(page.imageUrl!!, headers)
+    }
+
+    /**
+     * 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 url the full url to the chapter.
+     */
+    fun SChapter.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
+    }
+
+    /**
+     * 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 url the full url to the manga.
+     */
+    fun SManga.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
+    }
+
+    /**
+     * 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
+        }
+    }
+
+    /**
+     * 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].
+     *
+     * @param chapter the chapter to be added.
+     * @param manga the manga of the chapter.
+     */
+    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
+    }
+
+    /**
+     * Returns the list of filters for the source.
+     */
+    override fun getFilterList() = FilterList()
+}

+ 25 - 25
app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt

@@ -1,25 +1,25 @@
-package eu.kanade.tachiyomi.source.online
-
-import eu.kanade.tachiyomi.source.model.Page
-import rx.Observable
-
-fun HttpSource.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 }
-}
-
-fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
-    return Observable.from(pages)
-            .filter { !it.imageUrl.isNullOrEmpty() }
-            .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
-}
-
-fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
-    return Observable.from(pages)
-            .filter { it.imageUrl.isNullOrEmpty() }
-            .concatMap { getImageUrl(it) }
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.Page
+import rx.Observable
+
+fun HttpSource.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 }
+}
+
+fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+    return Observable.from(pages)
+            .filter { !it.imageUrl.isNullOrEmpty() }
+            .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
+}
+
+fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+    return Observable.from(pages)
+            .filter { it.imageUrl.isNullOrEmpty() }
+            .concatMap { getImageUrl(it) }
+}

+ 14 - 14
app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt

@@ -1,15 +1,15 @@
-package eu.kanade.tachiyomi.source.online
-
-import eu.kanade.tachiyomi.source.Source
-import okhttp3.Response
-import rx.Observable
-
-interface LoginSource : Source {
-
-    fun isLogged(): Boolean
-
-    fun login(username: String, password: String): Observable<Boolean>
-
-    fun isAuthenticationSuccessful(response: Response): Boolean
-
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.Source
+import okhttp3.Response
+import rx.Observable
+
+interface LoginSource : Source {
+
+    fun isLogged(): Boolean
+
+    fun login(username: String, password: String): Observable<Boolean>
+
+    fun isAuthenticationSuccessful(response: Response): Boolean
+
 }

+ 200 - 200
app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt

@@ -1,200 +1,200 @@
-package eu.kanade.tachiyomi.source.online
-
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.util.asJsoup
-import okhttp3.Response
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-
-/**
- * A simple implementation for sources from a website using Jsoup, an HTML parser.
- */
-abstract class ParsedHttpSource : HttpSource() {
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun popularMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(popularMangaSelector()).map { element ->
-            popularMangaFromElement(element)
-        }
-
-        val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
-            document.select(selector).first()
-        } != null
-
-        return MangasPage(mangas, hasNextPage)
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun popularMangaSelector(): String
-
-    /**
-     * 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].
-     */
-    abstract protected fun popularMangaFromElement(element: Element): SManga
-
-    /**
-     * 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 popularMangaNextPageSelector(): String?
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun searchMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(searchMangaSelector()).map { element ->
-            searchMangaFromElement(element)
-        }
-
-        val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
-            document.select(selector).first()
-        } != null
-
-        return MangasPage(mangas, hasNextPage)
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun searchMangaSelector(): String
-
-    /**
-     * 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].
-     */
-    abstract protected fun searchMangaFromElement(element: Element): SManga
-
-    /**
-     * 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 searchMangaNextPageSelector(): String?
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun latestUpdatesParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        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 that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun latestUpdatesSelector(): String
-
-    /**
-     * 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): SManga
-
-    /**
-     * 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?
-
-    /**
-     * Parses the response from the site and returns the details of a manga.
-     *
-     * @param response the response from the site.
-     */
-    override fun mangaDetailsParse(response: Response): SManga {
-        return mangaDetailsParse(response.asJsoup())
-    }
-
-    /**
-     * Returns the details of the manga from the given [document].
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun mangaDetailsParse(document: Document): SManga
-
-    /**
-     * Parses the response from the site and returns a list of chapters.
-     *
-     * @param response the response from the site.
-     */
-    override fun chapterListParse(response: Response): List<SChapter> {
-        val document = response.asJsoup()
-        return document.select(chapterListSelector()).map { chapterFromElement(it) }
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
-     */
-    abstract protected fun chapterListSelector(): String
-
-    /**
-     * Returns a chapter from the given element.
-     *
-     * @param element an element obtained from [chapterListSelector].
-     */
-    abstract protected fun chapterFromElement(element: Element): SChapter
-
-    /**
-     * Parses the response from the site and returns the page list.
-     *
-     * @param response the response from the site.
-     */
-    override fun pageListParse(response: Response): List<Page> {
-        return pageListParse(response.asJsoup())
-    }
-
-    /**
-     * Returns a page list from the given document.
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun pageListParse(document: Document): List<Page>
-
-    /**
-     * Parse the response from the site and returns the absolute url to the source image.
-     *
-     * @param response the response from the site.
-     */
-    override fun imageUrlParse(response: Response): String {
-        return imageUrlParse(response.asJsoup())
-    }
-
-    /**
-     * Returns the absolute url to the source image from the document.
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun imageUrlParse(document: Document): String
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+/**
+ * A simple implementation for sources from a website using Jsoup, an HTML parser.
+ */
+abstract class ParsedHttpSource : HttpSource() {
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun popularMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(popularMangaSelector()).map { element ->
+            popularMangaFromElement(element)
+        }
+
+        val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun popularMangaSelector(): String
+
+    /**
+     * 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].
+     */
+    abstract protected fun popularMangaFromElement(element: Element): SManga
+
+    /**
+     * 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 popularMangaNextPageSelector(): String?
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun searchMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(searchMangaSelector()).map { element ->
+            searchMangaFromElement(element)
+        }
+
+        val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun searchMangaSelector(): String
+
+    /**
+     * 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].
+     */
+    abstract protected fun searchMangaFromElement(element: Element): SManga
+
+    /**
+     * 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 searchMangaNextPageSelector(): String?
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun latestUpdatesParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        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 that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun latestUpdatesSelector(): String
+
+    /**
+     * 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): SManga
+
+    /**
+     * 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?
+
+    /**
+     * Parses the response from the site and returns the details of a manga.
+     *
+     * @param response the response from the site.
+     */
+    override fun mangaDetailsParse(response: Response): SManga {
+        return mangaDetailsParse(response.asJsoup())
+    }
+
+    /**
+     * Returns the details of the manga from the given [document].
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun mangaDetailsParse(document: Document): SManga
+
+    /**
+     * Parses the response from the site and returns a list of chapters.
+     *
+     * @param response the response from the site.
+     */
+    override fun chapterListParse(response: Response): List<SChapter> {
+        val document = response.asJsoup()
+        return document.select(chapterListSelector()).map { chapterFromElement(it) }
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
+     */
+    abstract protected fun chapterListSelector(): String
+
+    /**
+     * Returns a chapter from the given element.
+     *
+     * @param element an element obtained from [chapterListSelector].
+     */
+    abstract protected fun chapterFromElement(element: Element): SChapter
+
+    /**
+     * Parses the response from the site and returns the page list.
+     *
+     * @param response the response from the site.
+     */
+    override fun pageListParse(response: Response): List<Page> {
+        return pageListParse(response.asJsoup())
+    }
+
+    /**
+     * Returns a page list from the given document.
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun pageListParse(document: Document): List<Page>
+
+    /**
+     * Parse the response from the site and returns the absolute url to the source image.
+     *
+     * @param response the response from the site.
+     */
+    override fun imageUrlParse(response: Response): String {
+        return imageUrlParse(response.asJsoup())
+    }
+
+    /**
+     * Returns the absolute url to the source image from the document.
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun imageUrlParse(document: Document): String
+}

+ 21 - 21
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt

@@ -1,21 +1,21 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import android.os.Bundle
-import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
-import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
-import nucleus.factory.PresenterFactory
-import nucleus.presenter.Presenter
-
-@Suppress("LeakingThis")
-abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
-        PresenterFactory<P> {
-
-    private val delegate = NucleusConductorDelegate(this)
-
-    val presenter: P
-        get() = delegate.presenter
-
-    init {
-        addLifecycleListener(NucleusConductorLifecycleListener(delegate))
-    }
-}
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
+import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
+import nucleus.factory.PresenterFactory
+import nucleus.presenter.Presenter
+
+@Suppress("LeakingThis")
+abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
+        PresenterFactory<P> {
+
+    private val delegate = NucleusConductorDelegate(this)
+
+    val presenter: P
+        get() = delegate.presenter
+
+    init {
+        addLifecycleListener(NucleusConductorLifecycleListener(delegate))
+    }
+}

+ 61 - 61
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java

@@ -1,61 +1,61 @@
-package eu.kanade.tachiyomi.ui.base.presenter;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-
-import nucleus.factory.PresenterFactory;
-import nucleus.presenter.Presenter;
-
-public class NucleusConductorDelegate<P extends Presenter> {
-
-    @Nullable private P presenter;
-    @Nullable private Bundle bundle;
-
-    private PresenterFactory<P> factory;
-
-    public NucleusConductorDelegate(PresenterFactory<P> creator) {
-        this.factory = creator;
-    }
-
-    public P getPresenter() {
-        if (presenter == null) {
-            presenter = factory.createPresenter();
-            presenter.create(bundle);
-            bundle = null;
-        }
-        return presenter;
-    }
-
-    Bundle onSaveInstanceState() {
-        Bundle bundle = new Bundle();
-//        getPresenter(); // Workaround a crash related to saving instance state with child routers
-        if (presenter != null) {
-            presenter.save(bundle);
-        }
-        return bundle;
-    }
-
-    void onRestoreInstanceState(Bundle presenterState) {
-        bundle = presenterState;
-    }
-
-    void onTakeView(Object view) {
-        getPresenter();
-        if (presenter != null) {
-            //noinspection unchecked
-            presenter.takeView(view);
-        }
-    }
-
-    void onDropView() {
-        if (presenter != null) {
-            presenter.dropView();
-        }
-    }
-
-    void onDestroy() {
-        if (presenter != null) {
-            presenter.destroy();
-        }
-    }
-}
+package eu.kanade.tachiyomi.ui.base.presenter;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+import nucleus.factory.PresenterFactory;
+import nucleus.presenter.Presenter;
+
+public class NucleusConductorDelegate<P extends Presenter> {
+
+    @Nullable private P presenter;
+    @Nullable private Bundle bundle;
+
+    private PresenterFactory<P> factory;
+
+    public NucleusConductorDelegate(PresenterFactory<P> creator) {
+        this.factory = creator;
+    }
+
+    public P getPresenter() {
+        if (presenter == null) {
+            presenter = factory.createPresenter();
+            presenter.create(bundle);
+            bundle = null;
+        }
+        return presenter;
+    }
+
+    Bundle onSaveInstanceState() {
+        Bundle bundle = new Bundle();
+//        getPresenter(); // Workaround a crash related to saving instance state with child routers
+        if (presenter != null) {
+            presenter.save(bundle);
+        }
+        return bundle;
+    }
+
+    void onRestoreInstanceState(Bundle presenterState) {
+        bundle = presenterState;
+    }
+
+    void onTakeView(Object view) {
+        getPresenter();
+        if (presenter != null) {
+            //noinspection unchecked
+            presenter.takeView(view);
+        }
+    }
+
+    void onDropView() {
+        if (presenter != null) {
+            presenter.dropView();
+        }
+    }
+
+    void onDestroy() {
+        if (presenter != null) {
+            presenter.destroy();
+        }
+    }
+}

+ 44 - 44
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java

@@ -1,44 +1,44 @@
-package eu.kanade.tachiyomi.ui.base.presenter;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.view.View;
-
-import com.bluelinelabs.conductor.Controller;
-
-public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
-
-    private static final String PRESENTER_STATE_KEY = "presenter_state";
-
-    private NucleusConductorDelegate delegate;
-
-    public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
-        this.delegate = delegate;
-    }
-
-    @Override
-    public void postCreateView(@NonNull Controller controller, @NonNull View view) {
-        delegate.onTakeView(controller);
-    }
-
-    @Override
-    public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
-        delegate.onDropView();
-    }
-
-    @Override
-    public void preDestroy(@NonNull Controller controller) {
-        delegate.onDestroy();
-    }
-
-    @Override
-    public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
-        outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
-    }
-
-    @Override
-    public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
-        delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
-    }
-
-}
+package eu.kanade.tachiyomi.ui.base.presenter;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+import com.bluelinelabs.conductor.Controller;
+
+public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
+
+    private static final String PRESENTER_STATE_KEY = "presenter_state";
+
+    private NucleusConductorDelegate delegate;
+
+    public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void postCreateView(@NonNull Controller controller, @NonNull View view) {
+        delegate.onTakeView(controller);
+    }
+
+    @Override
+    public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
+        delegate.onDropView();
+    }
+
+    @Override
+    public void preDestroy(@NonNull Controller controller) {
+        delegate.onDestroy();
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
+        outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
+    }
+
+    @Override
+    public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
+        delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
+    }
+
+}

+ 88 - 88
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt

@@ -1,88 +1,88 @@
-package eu.kanade.tachiyomi.ui.catalogue.filter
-
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.kanade.tachiyomi.source.model.Filter
-
-class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as TriStateSectionItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-}
-
-class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as TextSectionItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-}
-
-class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as CheckboxSectionItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-}
-
-class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as SelectSectionItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-}
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.kanade.tachiyomi.source.model.Filter
+
+class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return filter == (other as TriStateSectionItem).filter
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+}
+
+class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return filter == (other as TextSectionItem).filter
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+}
+
+class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return filter == (other as CheckboxSectionItem).filter
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+}
+
+class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return filter == (other as SelectSectionItem).filter
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+}

+ 51 - 51
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt

@@ -1,52 +1,52 @@
-package eu.kanade.tachiyomi.ui.catalogue.filter
-
-import android.view.View
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.util.setVectorCompat
-
-class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
-
-    init {
-        isExpanded = false
-    }
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_group
-    }
-
-    override fun getItemViewType(): Int {
-        return 100
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        holder.title.text = filter.name
-
-        holder.icon.setVectorCompat(if (isExpanded)
-            R.drawable.ic_expand_more_white_24dp
-        else
-            R.drawable.ic_chevron_right_white_24dp)
-
-        holder.itemView.setOnClickListener(holder)
-
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as SortGroup).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.util.setVectorCompat
+
+class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
+
+    init {
+        isExpanded = false
+    }
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_group
+    }
+
+    override fun getItemViewType(): Int {
+        return 100
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+        return Holder(view, adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        holder.title.text = filter.name
+
+        holder.icon.setVectorCompat(if (isExpanded)
+            R.drawable.ic_expand_more_white_24dp
+        else
+            R.drawable.ic_chevron_right_white_24dp)
+
+        holder.itemView.setOnClickListener(holder)
+
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return filter == (other as SortGroup).filter
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
 }

+ 27 - 27
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt

@@ -1,28 +1,28 @@
-package eu.kanade.tachiyomi.ui.catalogue.global_search
-
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-/**
- * Adapter that holds the manga items from search results.
- *
- * @param controller instance of [CatalogueSearchController].
- */
-class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
-        FlexibleAdapter<CatalogueSearchCardItem>(null, controller, true) {
-
-    /**
-     * Listen for browse item clicks.
-     */
-    val mangaClickListener: OnMangaClickListener = controller
-
-    /**
-     * Listener which should be called when user clicks browse.
-     * Note: Should only be handled by [CatalogueSearchController]
-     */
-    interface OnMangaClickListener {
-        fun onMangaClick(manga: Manga)
-        fun onMangaLongClick(manga: Manga)
-    }
-
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Adapter that holds the manga items from search results.
+ *
+ * @param controller instance of [CatalogueSearchController].
+ */
+class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
+        FlexibleAdapter<CatalogueSearchCardItem>(null, controller, true) {
+
+    /**
+     * Listen for browse item clicks.
+     */
+    val mangaClickListener: OnMangaClickListener = controller
+
+    /**
+     * Listener which should be called when user clicks browse.
+     * Note: Should only be handled by [CatalogueSearchController]
+     */
+    interface OnMangaClickListener {
+        fun onMangaClick(manga: Manga)
+        fun onMangaLongClick(manga: Manga)
+    }
+
 }

+ 51 - 51
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt

@@ -1,52 +1,52 @@
-package eu.kanade.tachiyomi.ui.catalogue.global_search
-
-import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
-import eu.kanade.tachiyomi.widget.StateImageViewTarget
-import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.*
-
-class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
-    : BaseFlexibleViewHolder(view, adapter) {
-
-    init {
-        // Call onMangaClickListener when item is pressed.
-        itemView.setOnClickListener {
-            val item = adapter.getItem(adapterPosition)
-            if (item != null) {
-                adapter.mangaClickListener.onMangaClick(item.manga)
-            }
-        }
-        itemView.setOnLongClickListener {
-            val item = adapter.getItem(adapterPosition)
-            if (item != null) {
-                adapter.mangaClickListener.onMangaLongClick(item.manga)
-            }
-            true
-        }
-    }
-
-    fun bind(manga: Manga) {
-        tvTitle.text = manga.title
-        // Set alpha of thumbnail.
-        itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
-
-        setImage(manga)
-    }
-
-    fun setImage(manga: Manga) {
-        GlideApp.with(itemView.context).clear(itemImage)
-        if (!manga.thumbnail_url.isNullOrEmpty()) {
-            GlideApp.with(itemView.context)
-                    .load(manga)
-                    .diskCacheStrategy(DiskCacheStrategy.DATA)
-                    .centerCrop()
-                    .skipMemoryCache(true)
-                    .placeholder(android.R.color.transparent)
-                    .into(StateImageViewTarget(itemImage, progress))
-        }
-    }
-
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
+import eu.kanade.tachiyomi.widget.StateImageViewTarget
+import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.*
+
+class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
+    : BaseFlexibleViewHolder(view, adapter) {
+
+    init {
+        // Call onMangaClickListener when item is pressed.
+        itemView.setOnClickListener {
+            val item = adapter.getItem(adapterPosition)
+            if (item != null) {
+                adapter.mangaClickListener.onMangaClick(item.manga)
+            }
+        }
+        itemView.setOnLongClickListener {
+            val item = adapter.getItem(adapterPosition)
+            if (item != null) {
+                adapter.mangaClickListener.onMangaLongClick(item.manga)
+            }
+            true
+        }
+    }
+
+    fun bind(manga: Manga) {
+        tvTitle.text = manga.title
+        // Set alpha of thumbnail.
+        itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
+
+        setImage(manga)
+    }
+
+    fun setImage(manga: Manga) {
+        GlideApp.with(itemView.context).clear(itemImage)
+        if (!manga.thumbnail_url.isNullOrEmpty()) {
+            GlideApp.with(itemView.context)
+                    .load(manga)
+                    .diskCacheStrategy(DiskCacheStrategy.DATA)
+                    .centerCrop()
+                    .skipMemoryCache(true)
+                    .placeholder(android.R.color.transparent)
+                    .into(StateImageViewTarget(itemImage, progress))
+        }
+    }
+
 }

+ 34 - 34
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt

@@ -1,35 +1,35 @@
-package eu.kanade.tachiyomi.ui.catalogue.global_search
-
-import android.view.View
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.catalogue_global_search_controller_card_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder {
-        return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
-                                position: Int, payloads: List<Any?>?) {
-        holder.bind(manga)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (other is CatalogueSearchCardItem) {
-            return manga.id == other.manga.id
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return manga.id?.toInt() ?: 0
-    }
-
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.catalogue_global_search_controller_card_item
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder {
+        return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
+                                position: Int, payloads: List<Any?>?) {
+        holder.bind(manga)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other is CatalogueSearchCardItem) {
+            return manga.id == other.manga.id
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return manga.id?.toInt() ?: 0
+    }
+
 }

+ 247 - 247
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt

@@ -1,247 +1,247 @@
-package eu.kanade.tachiyomi.ui.download
-
-import android.support.v7.widget.LinearLayoutManager
-import android.view.*
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.download.DownloadService
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import kotlinx.android.synthetic.main.download_controller.*
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import java.util.*
-import java.util.concurrent.TimeUnit
-
-/**
- * Controller that shows the currently active downloads.
- * Uses R.layout.fragment_download_queue.
- */
-class DownloadController : NucleusController<DownloadPresenter>() {
-
-    /**
-     * Adapter containing the active downloads.
-     */
-    private var adapter: DownloadAdapter? = null
-
-    /**
-     * Map of subscriptions for active downloads.
-     */
-    private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
-
-    /**
-     * Whether the download queue is running or not.
-     */
-    private var isRunning: Boolean = false
-
-    init {
-        setHasOptionsMenu(true)
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.download_controller, container, false)
-    }
-
-    override fun createPresenter(): DownloadPresenter {
-        return DownloadPresenter()
-    }
-
-    override fun getTitle(): String? {
-        return resources?.getString(R.string.label_download_queue)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        // Check if download queue is empty and update information accordingly.
-        setInformationView()
-
-        // Initialize adapter.
-        adapter = DownloadAdapter()
-        recycler.adapter = adapter
-
-        // Set the layout manager for the recycler and fixed size.
-        recycler.layoutManager = LinearLayoutManager(view.context)
-        recycler.setHasFixedSize(true)
-
-        // Suscribe to changes
-        DownloadService.runningRelay
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeUntilDestroy { onQueueStatusChange(it) }
-
-        presenter.getDownloadStatusObservable()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeUntilDestroy { onStatusChange(it) }
-
-        presenter.getDownloadProgressObservable()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeUntilDestroy { onUpdateDownloadedPages(it) }
-    }
-
-    override fun onDestroyView(view: View) {
-        for (subscription in progressSubscriptions.values) {
-            subscription.unsubscribe()
-        }
-        progressSubscriptions.clear()
-        adapter = null
-        super.onDestroyView(view)
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.download_queue, menu)
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        // Set start button visibility.
-        menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
-
-        // Set pause button visibility.
-        menu.findItem(R.id.pause_queue).isVisible = isRunning
-
-        // Set clear button visibility.
-        menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        val context = applicationContext ?: return false
-        when (item.itemId) {
-            R.id.start_queue -> DownloadService.start(context)
-            R.id.pause_queue -> {
-                DownloadService.stop(context)
-                presenter.pauseDownloads()
-            }
-            R.id.clear_queue -> {
-                DownloadService.stop(context)
-                presenter.clearQueue()
-            }
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    /**
-     * Called when the status of a download changes.
-     *
-     * @param download the download whose status has changed.
-     */
-    private fun onStatusChange(download: Download) {
-        when (download.status) {
-            Download.DOWNLOADING -> {
-                observeProgress(download)
-                // Initial update of the downloaded pages
-                onUpdateDownloadedPages(download)
-            }
-            Download.DOWNLOADED -> {
-                unsubscribeProgress(download)
-                onUpdateProgress(download)
-                onUpdateDownloadedPages(download)
-            }
-            Download.ERROR -> unsubscribeProgress(download)
-        }
-    }
-
-    /**
-     * Observe the progress of a download and notify the view.
-     *
-     * @param download the download to observe its progress.
-     */
-    private fun observeProgress(download: Download) {
-        val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
-                // Get the sum of percentages for all the pages.
-                .flatMap {
-                    Observable.from(download.pages)
-                            .map(Page::progress)
-                            .reduce { x, y -> x + y }
-                }
-                // Keep only the latest emission to avoid backpressure.
-                .onBackpressureLatest()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe { progress ->
-                    // Update the view only if the progress has changed.
-                    if (download.totalProgress != progress) {
-                        download.totalProgress = progress
-                        onUpdateProgress(download)
-                    }
-                }
-
-        // Avoid leaking subscriptions
-        progressSubscriptions.remove(download)?.unsubscribe()
-
-        progressSubscriptions.put(download, subscription)
-    }
-
-    /**
-     * Unsubscribes the given download from the progress subscriptions.
-     *
-     * @param download the download to unsubscribe.
-     */
-    private fun unsubscribeProgress(download: Download) {
-        progressSubscriptions.remove(download)?.unsubscribe()
-    }
-
-    /**
-     * Called when the queue's status has changed. Updates the visibility of the buttons.
-     *
-     * @param running whether the queue is now running or not.
-     */
-    private fun onQueueStatusChange(running: Boolean) {
-        isRunning = running
-        activity?.invalidateOptionsMenu()
-
-        // Check if download queue is empty and update information accordingly.
-        setInformationView()
-    }
-
-    /**
-     * Called from the presenter to assign the downloads for the adapter.
-     *
-     * @param downloads the downloads from the queue.
-     */
-    fun onNextDownloads(downloads: List<Download>) {
-        activity?.invalidateOptionsMenu()
-        setInformationView()
-        adapter?.setItems(downloads)
-    }
-
-    /**
-     * Called when the progress of a download changes.
-     *
-     * @param download the download whose progress has changed.
-     */
-    fun onUpdateProgress(download: Download) {
-        getHolder(download)?.notifyProgress()
-    }
-
-    /**
-     * Called when a page of a download is downloaded.
-     *
-     * @param download the download whose page has been downloaded.
-     */
-    fun onUpdateDownloadedPages(download: Download) {
-        getHolder(download)?.notifyDownloadedPages()
-    }
-
-    /**
-     * Returns the holder for the given download.
-     *
-     * @param download the download to find.
-     * @return the holder of the download or null if it's not bound.
-     */
-    private fun getHolder(download: Download): DownloadHolder? {
-        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
-    }
-
-    /**
-     * Set information view when queue is empty
-     */
-    private fun setInformationView() {
-        if (presenter.downloadQueue.isEmpty()) {
-            empty_view?.show(R.drawable.ic_file_download_black_128dp,
-                    R.string.information_no_downloads)
-        } else {
-            empty_view?.hide()
-        }
-    }
-
-}
+package eu.kanade.tachiyomi.ui.download
+
+import android.support.v7.widget.LinearLayoutManager
+import android.view.*
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.download.DownloadService
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import kotlinx.android.synthetic.main.download_controller.*
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ * Controller that shows the currently active downloads.
+ * Uses R.layout.fragment_download_queue.
+ */
+class DownloadController : NucleusController<DownloadPresenter>() {
+
+    /**
+     * Adapter containing the active downloads.
+     */
+    private var adapter: DownloadAdapter? = null
+
+    /**
+     * Map of subscriptions for active downloads.
+     */
+    private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
+
+    /**
+     * Whether the download queue is running or not.
+     */
+    private var isRunning: Boolean = false
+
+    init {
+        setHasOptionsMenu(true)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.download_controller, container, false)
+    }
+
+    override fun createPresenter(): DownloadPresenter {
+        return DownloadPresenter()
+    }
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_download_queue)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        // Check if download queue is empty and update information accordingly.
+        setInformationView()
+
+        // Initialize adapter.
+        adapter = DownloadAdapter()
+        recycler.adapter = adapter
+
+        // Set the layout manager for the recycler and fixed size.
+        recycler.layoutManager = LinearLayoutManager(view.context)
+        recycler.setHasFixedSize(true)
+
+        // Suscribe to changes
+        DownloadService.runningRelay
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeUntilDestroy { onQueueStatusChange(it) }
+
+        presenter.getDownloadStatusObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeUntilDestroy { onStatusChange(it) }
+
+        presenter.getDownloadProgressObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeUntilDestroy { onUpdateDownloadedPages(it) }
+    }
+
+    override fun onDestroyView(view: View) {
+        for (subscription in progressSubscriptions.values) {
+            subscription.unsubscribe()
+        }
+        progressSubscriptions.clear()
+        adapter = null
+        super.onDestroyView(view)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.download_queue, menu)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        // Set start button visibility.
+        menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
+
+        // Set pause button visibility.
+        menu.findItem(R.id.pause_queue).isVisible = isRunning
+
+        // Set clear button visibility.
+        menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val context = applicationContext ?: return false
+        when (item.itemId) {
+            R.id.start_queue -> DownloadService.start(context)
+            R.id.pause_queue -> {
+                DownloadService.stop(context)
+                presenter.pauseDownloads()
+            }
+            R.id.clear_queue -> {
+                DownloadService.stop(context)
+                presenter.clearQueue()
+            }
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    /**
+     * Called when the status of a download changes.
+     *
+     * @param download the download whose status has changed.
+     */
+    private fun onStatusChange(download: Download) {
+        when (download.status) {
+            Download.DOWNLOADING -> {
+                observeProgress(download)
+                // Initial update of the downloaded pages
+                onUpdateDownloadedPages(download)
+            }
+            Download.DOWNLOADED -> {
+                unsubscribeProgress(download)
+                onUpdateProgress(download)
+                onUpdateDownloadedPages(download)
+            }
+            Download.ERROR -> unsubscribeProgress(download)
+        }
+    }
+
+    /**
+     * Observe the progress of a download and notify the view.
+     *
+     * @param download the download to observe its progress.
+     */
+    private fun observeProgress(download: Download) {
+        val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
+                // Get the sum of percentages for all the pages.
+                .flatMap {
+                    Observable.from(download.pages)
+                            .map(Page::progress)
+                            .reduce { x, y -> x + y }
+                }
+                // Keep only the latest emission to avoid backpressure.
+                .onBackpressureLatest()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe { progress ->
+                    // Update the view only if the progress has changed.
+                    if (download.totalProgress != progress) {
+                        download.totalProgress = progress
+                        onUpdateProgress(download)
+                    }
+                }
+
+        // Avoid leaking subscriptions
+        progressSubscriptions.remove(download)?.unsubscribe()
+
+        progressSubscriptions.put(download, subscription)
+    }
+
+    /**
+     * Unsubscribes the given download from the progress subscriptions.
+     *
+     * @param download the download to unsubscribe.
+     */
+    private fun unsubscribeProgress(download: Download) {
+        progressSubscriptions.remove(download)?.unsubscribe()
+    }
+
+    /**
+     * Called when the queue's status has changed. Updates the visibility of the buttons.
+     *
+     * @param running whether the queue is now running or not.
+     */
+    private fun onQueueStatusChange(running: Boolean) {
+        isRunning = running
+        activity?.invalidateOptionsMenu()
+
+        // Check if download queue is empty and update information accordingly.
+        setInformationView()
+    }
+
+    /**
+     * Called from the presenter to assign the downloads for the adapter.
+     *
+     * @param downloads the downloads from the queue.
+     */
+    fun onNextDownloads(downloads: List<Download>) {
+        activity?.invalidateOptionsMenu()
+        setInformationView()
+        adapter?.setItems(downloads)
+    }
+
+    /**
+     * Called when the progress of a download changes.
+     *
+     * @param download the download whose progress has changed.
+     */
+    fun onUpdateProgress(download: Download) {
+        getHolder(download)?.notifyProgress()
+    }
+
+    /**
+     * Called when a page of a download is downloaded.
+     *
+     * @param download the download whose page has been downloaded.
+     */
+    fun onUpdateDownloadedPages(download: Download) {
+        getHolder(download)?.notifyDownloadedPages()
+    }
+
+    /**
+     * Returns the holder for the given download.
+     *
+     * @param download the download to find.
+     * @return the holder of the download or null if it's not bound.
+     */
+    private fun getHolder(download: Download): DownloadHolder? {
+        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
+    }
+
+    /**
+     * Set information view when queue is empty
+     */
+    private fun setInformationView() {
+        if (presenter.downloadQueue.isEmpty()) {
+            empty_view?.show(R.drawable.ic_file_download_black_128dp,
+                    R.string.information_no_downloads)
+        } else {
+            empty_view?.hide()
+        }
+    }
+
+}

+ 47 - 47
app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt

@@ -1,48 +1,48 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
-        DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
-
-    private var mangas = emptyList<Manga>()
-
-    private var categories = emptyList<Category>()
-
-    private var preselected = emptyArray<Int>()
-
-    constructor(target: T, mangas: List<Manga>, categories: List<Category>,
-                preselected: Array<Int>) : this() {
-
-        this.mangas = mangas
-        this.categories = categories
-        this.preselected = preselected
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        return MaterialDialog.Builder(activity!!)
-                .title(R.string.action_move_category)
-                .items(categories.map { it.name })
-                .itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
-                    val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
-                    (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
-                    true
-                }
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .build()
-    }
-
-    interface Listener {
-        fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
-    }
-
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
+        DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
+
+    private var mangas = emptyList<Manga>()
+
+    private var categories = emptyList<Category>()
+
+    private var preselected = emptyArray<Int>()
+
+    constructor(target: T, mangas: List<Manga>, categories: List<Category>,
+                preselected: Array<Int>) : this() {
+
+        this.mangas = mangas
+        this.categories = categories
+        this.preselected = preselected
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_move_category)
+                .items(categories.map { it.name })
+                .itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
+                    val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
+                    (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
+                    true
+                }
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .build()
+    }
+
+    interface Listener {
+        fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
+    }
+
 }

+ 42 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt

@@ -1,43 +1,43 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.widget.DialogCheckboxView
-
-class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
-        DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
-
-    private var mangas = emptyList<Manga>()
-
-    constructor(target: T, mangas: List<Manga>) : this() {
-        this.mangas = mangas
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val view = DialogCheckboxView(activity!!).apply {
-            setDescription(R.string.confirm_delete_manga)
-            setOptionDescription(R.string.also_delete_chapters)
-        }
-
-        return MaterialDialog.Builder(activity!!)
-                .title(R.string.action_remove)
-                .customView(view, true)
-                .positiveText(android.R.string.yes)
-                .negativeText(android.R.string.no)
-                .onPositive { _, _ ->
-                    val deleteChapters = view.isChecked()
-                    (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
-                }
-                .build()
-    }
-
-    interface Listener {
-        fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
-    }
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.widget.DialogCheckboxView
+
+class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
+        DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
+
+    private var mangas = emptyList<Manga>()
+
+    constructor(target: T, mangas: List<Manga>) : this() {
+        this.mangas = mangas
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val view = DialogCheckboxView(activity!!).apply {
+            setDescription(R.string.confirm_delete_manga)
+            setOptionDescription(R.string.also_delete_chapters)
+        }
+
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_remove)
+                .customView(view, true)
+                .positiveText(android.R.string.yes)
+                .negativeText(android.R.string.no)
+                .onPositive { _, _ ->
+                    val deleteChapters = view.isChecked()
+                    (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
+    }
 }

+ 102 - 102
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt

@@ -1,103 +1,103 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
-
-/**
- * This adapter stores the categories from the library, used with a ViewPager.
- *
- * @constructor creates an instance of the adapter.
- */
-class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
-
-    /**
-     * The categories to bind in the adapter.
-     */
-    var categories: List<Category> = emptyList()
-        // This setter helps to not refresh the adapter if the reference to the list doesn't change.
-        set(value) {
-            if (field !== value) {
-                field = value
-                notifyDataSetChanged()
-            }
-        }
-
-    private var boundViews = arrayListOf<View>()
-
-    /**
-     * Creates a new view for this adapter.
-     *
-     * @return a new view.
-     */
-    override fun createView(container: ViewGroup): View {
-        val view = container.inflate(R.layout.library_category) as LibraryCategoryView
-        view.onCreate(controller)
-        return view
-    }
-
-    /**
-     * Binds a view with a position.
-     *
-     * @param view the view to bind.
-     * @param position the position in the adapter.
-     */
-    override fun bindView(view: View, position: Int) {
-        (view as LibraryCategoryView).onBind(categories[position])
-        boundViews.add(view)
-    }
-
-    /**
-     * Recycles a view.
-     *
-     * @param view the view to recycle.
-     * @param position the position in the adapter.
-     */
-    override fun recycleView(view: View, position: Int) {
-        (view as LibraryCategoryView).onRecycle()
-        boundViews.remove(view)
-    }
-
-    /**
-     * Returns the number of categories.
-     *
-     * @return the number of categories or 0 if the list is null.
-     */
-    override fun getCount(): Int {
-        return categories.size
-    }
-
-    /**
-     * Returns the title to display for a category.
-     *
-     * @param position the position of the element.
-     * @return the title to display.
-     */
-    override fun getPageTitle(position: Int): CharSequence {
-        return categories[position].name
-    }
-
-    /**
-     * Returns the position of the view.
-     */
-    override fun getItemPosition(obj: Any): Int {
-        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
-        val index = categories.indexOfFirst { it.id == view.category.id }
-        return if (index == -1) POSITION_NONE else index
-    }
-
-    /**
-     * Called when the view of this adapter is being destroyed.
-     */
-    fun onDestroy() {
-        for (view in boundViews) {
-            if (view is LibraryCategoryView) {
-                view.unsubscribe()
-            }
-        }
-    }
-
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
+
+/**
+ * This adapter stores the categories from the library, used with a ViewPager.
+ *
+ * @constructor creates an instance of the adapter.
+ */
+class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
+
+    /**
+     * The categories to bind in the adapter.
+     */
+    var categories: List<Category> = emptyList()
+        // This setter helps to not refresh the adapter if the reference to the list doesn't change.
+        set(value) {
+            if (field !== value) {
+                field = value
+                notifyDataSetChanged()
+            }
+        }
+
+    private var boundViews = arrayListOf<View>()
+
+    /**
+     * Creates a new view for this adapter.
+     *
+     * @return a new view.
+     */
+    override fun createView(container: ViewGroup): View {
+        val view = container.inflate(R.layout.library_category) as LibraryCategoryView
+        view.onCreate(controller)
+        return view
+    }
+
+    /**
+     * Binds a view with a position.
+     *
+     * @param view the view to bind.
+     * @param position the position in the adapter.
+     */
+    override fun bindView(view: View, position: Int) {
+        (view as LibraryCategoryView).onBind(categories[position])
+        boundViews.add(view)
+    }
+
+    /**
+     * Recycles a view.
+     *
+     * @param view the view to recycle.
+     * @param position the position in the adapter.
+     */
+    override fun recycleView(view: View, position: Int) {
+        (view as LibraryCategoryView).onRecycle()
+        boundViews.remove(view)
+    }
+
+    /**
+     * Returns the number of categories.
+     *
+     * @return the number of categories or 0 if the list is null.
+     */
+    override fun getCount(): Int {
+        return categories.size
+    }
+
+    /**
+     * Returns the title to display for a category.
+     *
+     * @param position the position of the element.
+     * @return the title to display.
+     */
+    override fun getPageTitle(position: Int): CharSequence {
+        return categories[position].name
+    }
+
+    /**
+     * Returns the position of the view.
+     */
+    override fun getItemPosition(obj: Any): Int {
+        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
+        val index = categories.indexOfFirst { it.id == view.category.id }
+        return if (index == -1) POSITION_NONE else index
+    }
+
+    /**
+     * Called when the view of this adapter is being destroyed.
+     */
+    fun onDestroy() {
+        for (view in boundViews) {
+            if (view is LibraryCategoryView) {
+                view.unsubscribe()
+            }
+        }
+    }
+
 }

+ 44 - 44
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt

@@ -1,44 +1,44 @@
-package eu.kanade.tachiyomi.ui.library
-
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-/**
- * Adapter storing a list of manga in a certain category.
- *
- * @param view the fragment containing this adapter.
- */
-class LibraryCategoryAdapter(view: LibraryCategoryView) :
-        FlexibleAdapter<LibraryItem>(null, view, true) {
-
-    /**
-     * The list of manga in this category.
-     */
-    private var mangas: List<LibraryItem> = emptyList()
-
-    /**
-     * Sets a list of manga in the adapter.
-     *
-     * @param list the list to set.
-     */
-    fun setItems(list: List<LibraryItem>) {
-        // A copy of manga always unfiltered.
-        mangas = list.toList()
-
-        performFilter()
-    }
-
-    /**
-     * Returns the position in the adapter for the given manga.
-     *
-     * @param manga the manga to find.
-     */
-    fun indexOf(manga: Manga): Int {
-        return currentItems.indexOfFirst { it.manga.id == manga.id }
-    }
-
-    fun performFilter() {
-        updateDataSet(mangas.filter { it.filter(searchText) })
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Adapter storing a list of manga in a certain category.
+ *
+ * @param view the fragment containing this adapter.
+ */
+class LibraryCategoryAdapter(view: LibraryCategoryView) :
+        FlexibleAdapter<LibraryItem>(null, view, true) {
+
+    /**
+     * The list of manga in this category.
+     */
+    private var mangas: List<LibraryItem> = emptyList()
+
+    /**
+     * Sets a list of manga in the adapter.
+     *
+     * @param list the list to set.
+     */
+    fun setItems(list: List<LibraryItem>) {
+        // A copy of manga always unfiltered.
+        mangas = list.toList()
+
+        performFilter()
+    }
+
+    /**
+     * Returns the position in the adapter for the given manga.
+     *
+     * @param manga the manga to find.
+     */
+    fun indexOf(manga: Manga): Int {
+        return currentItems.indexOfFirst { it.manga.id == manga.id }
+    }
+
+    fun performFilter() {
+        updateDataSet(mangas.filter { it.filter(searchText) })
+    }
+
+}

+ 247 - 247
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt

@@ -1,247 +1,247 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.content.Context
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.SelectableAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.util.plusAssign
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import kotlinx.android.synthetic.main.library_category.view.*
-import rx.subscriptions.CompositeSubscription
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Fragment containing the library manga for a certain category.
- */
-class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-        FrameLayout(context, attrs),
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener {
-
-    /**
-     * Preferences.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * The fragment containing this view.
-     */
-    private lateinit var controller: LibraryController
-
-    /**
-     * Category for this view.
-     */
-    lateinit var category: Category
-        private set
-
-    /**
-     * Recycler view of the list of manga.
-     */
-    private lateinit var recycler: RecyclerView
-
-    /**
-     * Adapter to hold the manga in this category.
-     */
-    private lateinit var adapter: LibraryCategoryAdapter
-
-    /**
-     * Subscriptions while the view is bound.
-     */
-    private var subscriptions = CompositeSubscription()
-
-    fun onCreate(controller: LibraryController) {
-        this.controller = controller
-
-        recycler = if (preferences.libraryAsList().getOrDefault()) {
-            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
-                layoutManager = LinearLayoutManager(context)
-            }
-        } else {
-            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
-                spanCount = controller.mangaPerRow
-            }
-        }
-
-        adapter = LibraryCategoryAdapter(this)
-
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-        swipe_refresh.addView(recycler)
-
-        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
-                // Disable swipe refresh when view is not at the top
-                val firstPos = (recycler.layoutManager as LinearLayoutManager)
-                        .findFirstCompletelyVisibleItemPosition()
-                swipe_refresh.isEnabled = firstPos <= 0
-            }
-        })
-
-        // Double the distance required to trigger sync
-        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
-        swipe_refresh.setOnRefreshListener {
-            if (!LibraryUpdateService.isRunning(context)) {
-                LibraryUpdateService.start(context, category)
-                context.toast(R.string.updating_category)
-            }
-            // It can be a very long operation, so we disable swipe refresh and show a toast.
-            swipe_refresh.isRefreshing = false
-        }
-    }
-
-    fun onBind(category: Category) {
-        this.category = category
-
-        adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
-            SelectableAdapter.Mode.MULTI
-        } else {
-            SelectableAdapter.Mode.SINGLE
-        }
-
-        subscriptions += controller.searchRelay
-                .doOnNext { adapter.searchText = it }
-                .skip(1)
-                .subscribe { adapter.performFilter() }
-
-        subscriptions += controller.libraryMangaRelay
-                .subscribe { onNextLibraryManga(it) }
-
-        subscriptions += controller.selectionRelay
-                .subscribe { onSelectionChanged(it) }
-    }
-
-    fun onRecycle() {
-        adapter.setItems(emptyList())
-        adapter.clearSelection()
-        unsubscribe()
-    }
-
-    fun unsubscribe() {
-        subscriptions.clear()
-    }
-
-    /**
-     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
-     * adapter.
-     *
-     * @param event the event received.
-     */
-    fun onNextLibraryManga(event: LibraryMangaEvent) {
-        // Get the manga list for this category.
-        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
-
-        // Update the category with its manga.
-        adapter.setItems(mangaForCategory)
-
-        if (adapter.mode == SelectableAdapter.Mode.MULTI) {
-            controller.selectedMangas.forEach { manga ->
-                val position = adapter.indexOf(manga)
-                if (position != -1 && !adapter.isSelected(position)) {
-                    adapter.toggleSelection(position)
-                    (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
-                }
-            }
-        }
-    }
-
-    /**
-     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
-     * depending on the type of event received.
-     *
-     * @param event the selection event received.
-     */
-    private fun onSelectionChanged(event: LibrarySelectionEvent) {
-        when (event) {
-            is LibrarySelectionEvent.Selected -> {
-                if (adapter.mode != SelectableAdapter.Mode.MULTI) {
-                    adapter.mode = SelectableAdapter.Mode.MULTI
-                }
-                findAndToggleSelection(event.manga)
-            }
-            is LibrarySelectionEvent.Unselected -> {
-                findAndToggleSelection(event.manga)
-                if (controller.selectedMangas.isEmpty()) {
-                    adapter.mode = SelectableAdapter.Mode.SINGLE
-                }
-            }
-            is LibrarySelectionEvent.Cleared -> {
-                adapter.mode = SelectableAdapter.Mode.SINGLE
-                adapter.clearSelection()
-            }
-        }
-    }
-
-    /**
-     * Toggles the selection for the given manga and updates the view if needed.
-     *
-     * @param manga the manga to toggle.
-     */
-    private fun findAndToggleSelection(manga: Manga) {
-        val position = adapter.indexOf(manga)
-        if (position != -1) {
-            adapter.toggleSelection(position)
-            (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
-        }
-    }
-
-    /**
-     * Called when a manga is clicked.
-     *
-     * @param position the position of the element clicked.
-     * @return true if the item should be selected, false otherwise.
-     */
-    override fun onItemClick(position: Int): Boolean {
-        // If the action mode is created and the position is valid, toggle the selection.
-        val item = adapter.getItem(position) ?: return false
-        if (adapter.mode == SelectableAdapter.Mode.MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openManga(item.manga)
-            return false
-        }
-    }
-
-    /**
-     * Called when a manga is long clicked.
-     *
-     * @param position the position of the element clicked.
-     */
-    override fun onItemLongClick(position: Int) {
-        controller.createActionModeIfNeeded()
-        toggleSelection(position)
-    }
-
-    /**
-     * Opens a manga.
-     *
-     * @param manga the manga to open.
-     */
-    private fun openManga(manga: Manga) {
-        controller.openManga(manga)
-    }
-
-    /**
-     * Tells the presenter to toggle the selection for the given position.
-     *
-     * @param position the position to toggle.
-     */
-    private fun toggleSelection(position: Int) {
-        val item = adapter.getItem(position) ?: return
-
-        controller.setSelection(item.manga, !adapter.isSelected(position))
-        controller.invalidateActionMode()
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.content.Context
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.util.plusAssign
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.widget.AutofitRecyclerView
+import kotlinx.android.synthetic.main.library_category.view.*
+import rx.subscriptions.CompositeSubscription
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Fragment containing the library manga for a certain category.
+ */
+class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+        FrameLayout(context, attrs),
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener {
+
+    /**
+     * Preferences.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    /**
+     * The fragment containing this view.
+     */
+    private lateinit var controller: LibraryController
+
+    /**
+     * Category for this view.
+     */
+    lateinit var category: Category
+        private set
+
+    /**
+     * Recycler view of the list of manga.
+     */
+    private lateinit var recycler: RecyclerView
+
+    /**
+     * Adapter to hold the manga in this category.
+     */
+    private lateinit var adapter: LibraryCategoryAdapter
+
+    /**
+     * Subscriptions while the view is bound.
+     */
+    private var subscriptions = CompositeSubscription()
+
+    fun onCreate(controller: LibraryController) {
+        this.controller = controller
+
+        recycler = if (preferences.libraryAsList().getOrDefault()) {
+            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
+                layoutManager = LinearLayoutManager(context)
+            }
+        } else {
+            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
+                spanCount = controller.mangaPerRow
+            }
+        }
+
+        adapter = LibraryCategoryAdapter(this)
+
+        recycler.setHasFixedSize(true)
+        recycler.adapter = adapter
+        swipe_refresh.addView(recycler)
+
+        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
+                // Disable swipe refresh when view is not at the top
+                val firstPos = (recycler.layoutManager as LinearLayoutManager)
+                        .findFirstCompletelyVisibleItemPosition()
+                swipe_refresh.isEnabled = firstPos <= 0
+            }
+        })
+
+        // Double the distance required to trigger sync
+        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
+        swipe_refresh.setOnRefreshListener {
+            if (!LibraryUpdateService.isRunning(context)) {
+                LibraryUpdateService.start(context, category)
+                context.toast(R.string.updating_category)
+            }
+            // It can be a very long operation, so we disable swipe refresh and show a toast.
+            swipe_refresh.isRefreshing = false
+        }
+    }
+
+    fun onBind(category: Category) {
+        this.category = category
+
+        adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
+            SelectableAdapter.Mode.MULTI
+        } else {
+            SelectableAdapter.Mode.SINGLE
+        }
+
+        subscriptions += controller.searchRelay
+                .doOnNext { adapter.searchText = it }
+                .skip(1)
+                .subscribe { adapter.performFilter() }
+
+        subscriptions += controller.libraryMangaRelay
+                .subscribe { onNextLibraryManga(it) }
+
+        subscriptions += controller.selectionRelay
+                .subscribe { onSelectionChanged(it) }
+    }
+
+    fun onRecycle() {
+        adapter.setItems(emptyList())
+        adapter.clearSelection()
+        unsubscribe()
+    }
+
+    fun unsubscribe() {
+        subscriptions.clear()
+    }
+
+    /**
+     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
+     * adapter.
+     *
+     * @param event the event received.
+     */
+    fun onNextLibraryManga(event: LibraryMangaEvent) {
+        // Get the manga list for this category.
+        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
+
+        // Update the category with its manga.
+        adapter.setItems(mangaForCategory)
+
+        if (adapter.mode == SelectableAdapter.Mode.MULTI) {
+            controller.selectedMangas.forEach { manga ->
+                val position = adapter.indexOf(manga)
+                if (position != -1 && !adapter.isSelected(position)) {
+                    adapter.toggleSelection(position)
+                    (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
+                }
+            }
+        }
+    }
+
+    /**
+     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
+     * depending on the type of event received.
+     *
+     * @param event the selection event received.
+     */
+    private fun onSelectionChanged(event: LibrarySelectionEvent) {
+        when (event) {
+            is LibrarySelectionEvent.Selected -> {
+                if (adapter.mode != SelectableAdapter.Mode.MULTI) {
+                    adapter.mode = SelectableAdapter.Mode.MULTI
+                }
+                findAndToggleSelection(event.manga)
+            }
+            is LibrarySelectionEvent.Unselected -> {
+                findAndToggleSelection(event.manga)
+                if (controller.selectedMangas.isEmpty()) {
+                    adapter.mode = SelectableAdapter.Mode.SINGLE
+                }
+            }
+            is LibrarySelectionEvent.Cleared -> {
+                adapter.mode = SelectableAdapter.Mode.SINGLE
+                adapter.clearSelection()
+            }
+        }
+    }
+
+    /**
+     * Toggles the selection for the given manga and updates the view if needed.
+     *
+     * @param manga the manga to toggle.
+     */
+    private fun findAndToggleSelection(manga: Manga) {
+        val position = adapter.indexOf(manga)
+        if (position != -1) {
+            adapter.toggleSelection(position)
+            (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
+        }
+    }
+
+    /**
+     * Called when a manga is clicked.
+     *
+     * @param position the position of the element clicked.
+     * @return true if the item should be selected, false otherwise.
+     */
+    override fun onItemClick(position: Int): Boolean {
+        // If the action mode is created and the position is valid, toggle the selection.
+        val item = adapter.getItem(position) ?: return false
+        if (adapter.mode == SelectableAdapter.Mode.MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openManga(item.manga)
+            return false
+        }
+    }
+
+    /**
+     * Called when a manga is long clicked.
+     *
+     * @param position the position of the element clicked.
+     */
+    override fun onItemLongClick(position: Int) {
+        controller.createActionModeIfNeeded()
+        toggleSelection(position)
+    }
+
+    /**
+     * Opens a manga.
+     *
+     * @param manga the manga to open.
+     */
+    private fun openManga(manga: Manga) {
+        controller.openManga(manga)
+    }
+
+    /**
+     * Tells the presenter to toggle the selection for the given position.
+     *
+     * @param position the position to toggle.
+     */
+    private fun toggleSelection(position: Int) {
+        val item = adapter.getItem(position) ?: return
+
+        controller.setSelection(item.manga, !adapter.isSelected(position))
+        controller.invalidateActionMode()
+    }
+
+}

+ 523 - 523
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -1,523 +1,523 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Activity
-import android.content.Intent
-import android.content.res.Configuration
-import android.graphics.Color
-import android.os.Bundle
-import android.support.design.widget.TabLayout
-import android.support.v4.graphics.drawable.DrawableCompat
-import android.support.v4.widget.DrawerLayout
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.SearchView
-import android.view.*
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import com.f2prateek.rx.preferences.Preference
-import com.jakewharton.rxbinding.support.v4.view.pageSelections
-import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
-import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
-import eu.kanade.tachiyomi.ui.base.controller.TabbedController
-import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
-import eu.kanade.tachiyomi.ui.category.CategoryController
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.migration.MigrationController
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.library_controller.*
-import kotlinx.android.synthetic.main.main_activity.*
-import rx.Subscription
-import timber.log.Timber
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.IOException
-
-
-class LibraryController(
-        bundle: Bundle? = null,
-        private val preferences: PreferencesHelper = Injekt.get()
-) : NucleusController<LibraryPresenter>(bundle),
-        TabbedController,
-        SecondaryDrawerController,
-        ActionMode.Callback,
-        ChangeMangaCategoriesDialog.Listener,
-        DeleteLibraryMangasDialog.Listener {
-
-    /**
-     * Position of the active category.
-     */
-    var activeCategory: Int = preferences.lastUsedCategory().getOrDefault()
-        private set
-
-    /**
-     * Action mode for selections.
-     */
-    private var actionMode: ActionMode? = null
-
-    /**
-     * Library search query.
-     */
-    private var query = ""
-
-    /**
-     * Currently selected mangas.
-     */
-    val selectedMangas = mutableSetOf<Manga>()
-
-    private var selectedCoverManga: Manga? = null
-
-    /**
-     * Relay to notify the UI of selection updates.
-     */
-    val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
-
-    /**
-     * Relay to notify search query changes.
-     */
-    val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
-
-    /**
-     * Relay to notify the library's viewpager for updates.
-     */
-    val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
-
-    /**
-     * Number of manga per row in grid mode.
-     */
-    var mangaPerRow = 0
-        private set
-
-    /**
-     * Adapter of the view pager.
-     */
-    private var adapter: LibraryAdapter? = null
-
-    /**
-     * Navigation view containing filter/sort/display items.
-     */
-    private var navView: LibraryNavigationView? = null
-
-    /**
-     * Drawer listener to allow swipe only for closing the drawer.
-     */
-    private var drawerListener: DrawerLayout.DrawerListener? = null
-
-    private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
-
-    private var tabsVisibilitySubscription: Subscription? = null
-
-    private var searchViewSubscription: Subscription? = null
-
-    init {
-        setHasOptionsMenu(true)
-        retainViewMode = RetainViewMode.RETAIN_DETACH
-    }
-
-    override fun getTitle(): String? {
-        return resources?.getString(R.string.label_library)
-    }
-
-    override fun createPresenter(): LibraryPresenter {
-        return LibraryPresenter()
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.library_controller, container, false)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        adapter = LibraryAdapter(this)
-        library_pager.adapter = adapter
-        library_pager.pageSelections().skip(1).subscribeUntilDestroy {
-            preferences.lastUsedCategory().set(it)
-            activeCategory = it
-        }
-
-        getColumnsPreferenceForCurrentOrientation().asObservable()
-                .doOnNext { mangaPerRow = it }
-                .skip(1)
-                // Set again the adapter to recalculate the covers height
-                .subscribeUntilDestroy { reattachAdapter() }
-
-        if (selectedMangas.isNotEmpty()) {
-            createActionModeIfNeeded()
-        }
-    }
-
-    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        super.onChangeStarted(handler, type)
-        if (type.isEnter) {
-            activity?.tabs?.setupWithViewPager(library_pager)
-            presenter.subscribeLibrary()
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        adapter?.onDestroy()
-        adapter = null
-        actionMode = null
-        tabsVisibilitySubscription?.unsubscribe()
-        tabsVisibilitySubscription = null
-        super.onDestroyView(view)
-    }
-
-    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
-        val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
-        navView = view
-        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
-
-        navView?.onGroupClicked = { group ->
-            when (group) {
-                is LibraryNavigationView.FilterGroup -> onFilterChanged()
-                is LibraryNavigationView.SortGroup -> onSortChanged()
-                is LibraryNavigationView.DisplayGroup -> reattachAdapter()
-                is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
-            }
-        }
-
-        return view
-    }
-
-    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
-        navView = null
-    }
-
-    override fun configureTabs(tabs: TabLayout) {
-        with(tabs) {
-            tabGravity = TabLayout.GRAVITY_CENTER
-            tabMode = TabLayout.MODE_SCROLLABLE
-        }
-        tabsVisibilitySubscription?.unsubscribe()
-        tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
-            val tabAnimator = (activity as? MainActivity)?.tabAnimator
-            if (visible) {
-                tabAnimator?.expand()
-            } else {
-                tabAnimator?.collapse()
-            }
-        }
-    }
-
-    override fun cleanupTabs(tabs: TabLayout) {
-        tabsVisibilitySubscription?.unsubscribe()
-        tabsVisibilitySubscription = null
-    }
-
-    fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
-        val view = view ?: return
-        val adapter = adapter ?: return
-
-        // Show empty view if needed
-        if (mangaMap.isNotEmpty()) {
-            empty_view.hide()
-        } else {
-            empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
-        }
-
-        // Get the current active category.
-        val activeCat = if (adapter.categories.isNotEmpty())
-            library_pager.currentItem
-        else
-            activeCategory
-
-        // Set the categories
-        adapter.categories = categories
-
-        // Restore active category.
-        library_pager.setCurrentItem(activeCat, false)
-
-        tabsVisibilityRelay.call(categories.size > 1)
-
-        // Delay the scroll position to allow the view to be properly measured.
-        view.post {
-            if (isAttached) {
-                activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true)
-            }
-        }
-
-        // Send the manga map to child fragments after the adapter is updated.
-        libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
-    }
-
-    /**
-     * Returns a preference for the number of manga per row based on the current orientation.
-     *
-     * @return the preference.
-     */
-    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
-        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
-            preferences.portraitColumns()
-        else
-            preferences.landscapeColumns()
-    }
-
-    /**
-     * Called when a filter is changed.
-     */
-    private fun onFilterChanged() {
-        presenter.requestFilterUpdate()
-        activity?.invalidateOptionsMenu()
-    }
-
-    private fun onDownloadBadgeChanged() {
-        presenter.requestDownloadBadgesUpdate()
-    }
-
-    /**
-     * Called when the sorting mode is changed.
-     */
-    private fun onSortChanged() {
-        presenter.requestSortUpdate()
-    }
-
-    /**
-     * Reattaches the adapter to the view pager to recreate fragments
-     */
-    private fun reattachAdapter() {
-        val adapter = adapter ?: return
-
-        val position = library_pager.currentItem
-
-        adapter.recycle = false
-        library_pager.adapter = adapter
-        library_pager.currentItem = position
-        adapter.recycle = true
-    }
-
-    /**
-     * Creates the action mode if it's not created already.
-     */
-    fun createActionModeIfNeeded() {
-        if (actionMode == null) {
-            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
-        }
-    }
-
-    /**
-     * Destroys the action mode.
-     */
-    fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.library, menu)
-
-        val searchItem = menu.findItem(R.id.action_search)
-        val searchView = searchItem.actionView as SearchView
-
-        if (!query.isEmpty()) {
-            searchItem.expandActionView()
-            searchView.setQuery(query, true)
-            searchView.clearFocus()
-        }
-
-        // Mutate the filter icon because it needs to be tinted and the resource is shared.
-        menu.findItem(R.id.action_filter).icon.mutate()
-
-        searchViewSubscription?.unsubscribe()
-        searchViewSubscription = searchView.queryTextChanges()
-                // Ignore events if this controller isn't at the top
-                .filter { router.backstack.lastOrNull()?.controller() == this }
-                .subscribeUntilDestroy {
-                    query = it.toString()
-                    searchRelay.call(query)
-                }
-
-        searchItem.fixExpand()
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        val navView = navView ?: return
-
-        val filterItem = menu.findItem(R.id.action_filter)
-
-        // Tint icon if there's a filter active
-        val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
-        DrawableCompat.setTint(filterItem.icon, filterColor)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_filter -> {
-                navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
-            }
-            R.id.action_update_library -> {
-                activity?.let { LibraryUpdateService.start(it) }
-            }
-            R.id.action_edit_categories -> {
-                router.pushController(CategoryController().withFadeTransaction())
-            }
-            R.id.action_source_migration -> {
-                router.pushController(MigrationController().withFadeTransaction())
-            }
-            else -> return super.onOptionsItemSelected(item)
-        }
-
-        return true
-    }
-
-    /**
-     * Invalidates the action mode, forcing it to refresh its content.
-     */
-    fun invalidateActionMode() {
-        actionMode?.invalidate()
-    }
-
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.library_selection, menu)
-        return true
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = selectedMangas.size
-        if (count == 0) {
-            // Destroy action mode if there are no items selected.
-            destroyActionModeIfNeeded()
-        } else {
-            mode.title = resources?.getString(R.string.label_selected, count)
-            menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
-        }
-        return false
-    }
-
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_edit_cover -> {
-                changeSelectedCover()
-                destroyActionModeIfNeeded()
-            }
-            R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
-            R.id.action_delete -> showDeleteMangaDialog()
-            else -> return false
-        }
-        return true
-    }
-
-    override fun onDestroyActionMode(mode: ActionMode?) {
-        // Clear all the manga selections and notify child views.
-        selectedMangas.clear()
-        selectionRelay.call(LibrarySelectionEvent.Cleared())
-        actionMode = null
-    }
-
-    fun openManga(manga: Manga) {
-        // Notify the presenter a manga is being opened.
-        presenter.onOpenManga()
-
-        router.pushController(MangaController(manga).withFadeTransaction())
-    }
-
-    /**
-     * Sets the selection for a given manga.
-     *
-     * @param manga the manga whose selection has changed.
-     * @param selected whether it's now selected or not.
-     */
-    fun setSelection(manga: Manga, selected: Boolean) {
-        if (selected) {
-            if (selectedMangas.add(manga)) {
-                selectionRelay.call(LibrarySelectionEvent.Selected(manga))
-            }
-        } else {
-            if (selectedMangas.remove(manga)) {
-                selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
-            }
-        }
-    }
-
-    /**
-     * Move the selected manga to a list of categories.
-     */
-    private fun showChangeMangaCategoriesDialog() {
-        // Create a copy of selected manga
-        val mangas = selectedMangas.toList()
-
-        // Hide the default category because it has a different behavior than the ones from db.
-        val categories = presenter.categories.filter { it.id != 0 }
-
-        // Get indexes of the common categories to preselect.
-        val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
-                .map { categories.indexOf(it) }
-                .toTypedArray()
-
-        ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
-                .showDialog(router)
-    }
-
-    private fun showDeleteMangaDialog() {
-        DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
-    }
-
-    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
-        presenter.moveMangasToCategories(categories, mangas)
-        destroyActionModeIfNeeded()
-    }
-
-    override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
-        presenter.removeMangaFromLibrary(mangas, deleteChapters)
-        destroyActionModeIfNeeded()
-    }
-
-    /**
-     * Changes the cover for the selected manga.
-     */
-    private fun changeSelectedCover() {
-        val manga = selectedMangas.firstOrNull() ?: return
-        selectedCoverManga = manga
-
-        if (manga.favorite) {
-            val intent = Intent(Intent.ACTION_GET_CONTENT)
-            intent.type = "image/*"
-            startActivityForResult(Intent.createChooser(intent,
-                    resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
-        } else {
-            activity?.toast(R.string.notification_first_add_to_library)
-        }
-    }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (requestCode == REQUEST_IMAGE_OPEN) {
-            if (data == null || resultCode != Activity.RESULT_OK) return
-            val activity = activity ?: return
-            val manga = selectedCoverManga ?: return
-
-            try {
-                // Get the file's input stream from the incoming Intent
-                activity.contentResolver.openInputStream(data.data).use {
-                    // Update cover to selected file, show error if something went wrong
-                    if (presenter.editCoverWithStream(it, manga)) {
-                        // TODO refresh cover
-                    } else {
-                        activity.toast(R.string.notification_cover_update_failed)
-                    }
-                }
-            } catch (error: IOException) {
-                activity.toast(R.string.notification_cover_update_failed)
-                Timber.e(error)
-            }
-            selectedCoverManga = null
-        }
-    }
-
-    private companion object {
-        /**
-         * Key to change the cover of a manga in [onActivityResult].
-         */
-        const val REQUEST_IMAGE_OPEN = 101
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Color
+import android.os.Bundle
+import android.support.design.widget.TabLayout
+import android.support.v4.graphics.drawable.DrawableCompat
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.f2prateek.rx.preferences.Preference
+import com.jakewharton.rxbinding.support.v4.view.pageSelections
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
+import eu.kanade.tachiyomi.ui.base.controller.TabbedController
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.category.CategoryController
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.migration.MigrationController
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.library_controller.*
+import kotlinx.android.synthetic.main.main_activity.*
+import rx.Subscription
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.IOException
+
+
+class LibraryController(
+        bundle: Bundle? = null,
+        private val preferences: PreferencesHelper = Injekt.get()
+) : NucleusController<LibraryPresenter>(bundle),
+        TabbedController,
+        SecondaryDrawerController,
+        ActionMode.Callback,
+        ChangeMangaCategoriesDialog.Listener,
+        DeleteLibraryMangasDialog.Listener {
+
+    /**
+     * Position of the active category.
+     */
+    var activeCategory: Int = preferences.lastUsedCategory().getOrDefault()
+        private set
+
+    /**
+     * Action mode for selections.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Library search query.
+     */
+    private var query = ""
+
+    /**
+     * Currently selected mangas.
+     */
+    val selectedMangas = mutableSetOf<Manga>()
+
+    private var selectedCoverManga: Manga? = null
+
+    /**
+     * Relay to notify the UI of selection updates.
+     */
+    val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
+
+    /**
+     * Relay to notify search query changes.
+     */
+    val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
+
+    /**
+     * Relay to notify the library's viewpager for updates.
+     */
+    val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
+
+    /**
+     * Number of manga per row in grid mode.
+     */
+    var mangaPerRow = 0
+        private set
+
+    /**
+     * Adapter of the view pager.
+     */
+    private var adapter: LibraryAdapter? = null
+
+    /**
+     * Navigation view containing filter/sort/display items.
+     */
+    private var navView: LibraryNavigationView? = null
+
+    /**
+     * Drawer listener to allow swipe only for closing the drawer.
+     */
+    private var drawerListener: DrawerLayout.DrawerListener? = null
+
+    private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
+
+    private var tabsVisibilitySubscription: Subscription? = null
+
+    private var searchViewSubscription: Subscription? = null
+
+    init {
+        setHasOptionsMenu(true)
+        retainViewMode = RetainViewMode.RETAIN_DETACH
+    }
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_library)
+    }
+
+    override fun createPresenter(): LibraryPresenter {
+        return LibraryPresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.library_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        adapter = LibraryAdapter(this)
+        library_pager.adapter = adapter
+        library_pager.pageSelections().skip(1).subscribeUntilDestroy {
+            preferences.lastUsedCategory().set(it)
+            activeCategory = it
+        }
+
+        getColumnsPreferenceForCurrentOrientation().asObservable()
+                .doOnNext { mangaPerRow = it }
+                .skip(1)
+                // Set again the adapter to recalculate the covers height
+                .subscribeUntilDestroy { reattachAdapter() }
+
+        if (selectedMangas.isNotEmpty()) {
+            createActionModeIfNeeded()
+        }
+    }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeStarted(handler, type)
+        if (type.isEnter) {
+            activity?.tabs?.setupWithViewPager(library_pager)
+            presenter.subscribeLibrary()
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        adapter?.onDestroy()
+        adapter = null
+        actionMode = null
+        tabsVisibilitySubscription?.unsubscribe()
+        tabsVisibilitySubscription = null
+        super.onDestroyView(view)
+    }
+
+    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
+        val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
+        navView = view
+        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
+
+        navView?.onGroupClicked = { group ->
+            when (group) {
+                is LibraryNavigationView.FilterGroup -> onFilterChanged()
+                is LibraryNavigationView.SortGroup -> onSortChanged()
+                is LibraryNavigationView.DisplayGroup -> reattachAdapter()
+                is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
+            }
+        }
+
+        return view
+    }
+
+    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
+        navView = null
+    }
+
+    override fun configureTabs(tabs: TabLayout) {
+        with(tabs) {
+            tabGravity = TabLayout.GRAVITY_CENTER
+            tabMode = TabLayout.MODE_SCROLLABLE
+        }
+        tabsVisibilitySubscription?.unsubscribe()
+        tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
+            val tabAnimator = (activity as? MainActivity)?.tabAnimator
+            if (visible) {
+                tabAnimator?.expand()
+            } else {
+                tabAnimator?.collapse()
+            }
+        }
+    }
+
+    override fun cleanupTabs(tabs: TabLayout) {
+        tabsVisibilitySubscription?.unsubscribe()
+        tabsVisibilitySubscription = null
+    }
+
+    fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
+        val view = view ?: return
+        val adapter = adapter ?: return
+
+        // Show empty view if needed
+        if (mangaMap.isNotEmpty()) {
+            empty_view.hide()
+        } else {
+            empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
+        }
+
+        // Get the current active category.
+        val activeCat = if (adapter.categories.isNotEmpty())
+            library_pager.currentItem
+        else
+            activeCategory
+
+        // Set the categories
+        adapter.categories = categories
+
+        // Restore active category.
+        library_pager.setCurrentItem(activeCat, false)
+
+        tabsVisibilityRelay.call(categories.size > 1)
+
+        // Delay the scroll position to allow the view to be properly measured.
+        view.post {
+            if (isAttached) {
+                activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true)
+            }
+        }
+
+        // Send the manga map to child fragments after the adapter is updated.
+        libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
+    }
+
+    /**
+     * Returns a preference for the number of manga per row based on the current orientation.
+     *
+     * @return the preference.
+     */
+    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
+        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
+            preferences.portraitColumns()
+        else
+            preferences.landscapeColumns()
+    }
+
+    /**
+     * Called when a filter is changed.
+     */
+    private fun onFilterChanged() {
+        presenter.requestFilterUpdate()
+        activity?.invalidateOptionsMenu()
+    }
+
+    private fun onDownloadBadgeChanged() {
+        presenter.requestDownloadBadgesUpdate()
+    }
+
+    /**
+     * Called when the sorting mode is changed.
+     */
+    private fun onSortChanged() {
+        presenter.requestSortUpdate()
+    }
+
+    /**
+     * Reattaches the adapter to the view pager to recreate fragments
+     */
+    private fun reattachAdapter() {
+        val adapter = adapter ?: return
+
+        val position = library_pager.currentItem
+
+        adapter.recycle = false
+        library_pager.adapter = adapter
+        library_pager.currentItem = position
+        adapter.recycle = true
+    }
+
+    /**
+     * Creates the action mode if it's not created already.
+     */
+    fun createActionModeIfNeeded() {
+        if (actionMode == null) {
+            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
+        }
+    }
+
+    /**
+     * Destroys the action mode.
+     */
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.library, menu)
+
+        val searchItem = menu.findItem(R.id.action_search)
+        val searchView = searchItem.actionView as SearchView
+
+        if (!query.isEmpty()) {
+            searchItem.expandActionView()
+            searchView.setQuery(query, true)
+            searchView.clearFocus()
+        }
+
+        // Mutate the filter icon because it needs to be tinted and the resource is shared.
+        menu.findItem(R.id.action_filter).icon.mutate()
+
+        searchViewSubscription?.unsubscribe()
+        searchViewSubscription = searchView.queryTextChanges()
+                // Ignore events if this controller isn't at the top
+                .filter { router.backstack.lastOrNull()?.controller() == this }
+                .subscribeUntilDestroy {
+                    query = it.toString()
+                    searchRelay.call(query)
+                }
+
+        searchItem.fixExpand()
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        val navView = navView ?: return
+
+        val filterItem = menu.findItem(R.id.action_filter)
+
+        // Tint icon if there's a filter active
+        val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
+        DrawableCompat.setTint(filterItem.icon, filterColor)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_filter -> {
+                navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
+            }
+            R.id.action_update_library -> {
+                activity?.let { LibraryUpdateService.start(it) }
+            }
+            R.id.action_edit_categories -> {
+                router.pushController(CategoryController().withFadeTransaction())
+            }
+            R.id.action_source_migration -> {
+                router.pushController(MigrationController().withFadeTransaction())
+            }
+            else -> return super.onOptionsItemSelected(item)
+        }
+
+        return true
+    }
+
+    /**
+     * Invalidates the action mode, forcing it to refresh its content.
+     */
+    fun invalidateActionMode() {
+        actionMode?.invalidate()
+    }
+
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.library_selection, menu)
+        return true
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = selectedMangas.size
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+            menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
+        }
+        return false
+    }
+
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_edit_cover -> {
+                changeSelectedCover()
+                destroyActionModeIfNeeded()
+            }
+            R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
+            R.id.action_delete -> showDeleteMangaDialog()
+            else -> return false
+        }
+        return true
+    }
+
+    override fun onDestroyActionMode(mode: ActionMode?) {
+        // Clear all the manga selections and notify child views.
+        selectedMangas.clear()
+        selectionRelay.call(LibrarySelectionEvent.Cleared())
+        actionMode = null
+    }
+
+    fun openManga(manga: Manga) {
+        // Notify the presenter a manga is being opened.
+        presenter.onOpenManga()
+
+        router.pushController(MangaController(manga).withFadeTransaction())
+    }
+
+    /**
+     * Sets the selection for a given manga.
+     *
+     * @param manga the manga whose selection has changed.
+     * @param selected whether it's now selected or not.
+     */
+    fun setSelection(manga: Manga, selected: Boolean) {
+        if (selected) {
+            if (selectedMangas.add(manga)) {
+                selectionRelay.call(LibrarySelectionEvent.Selected(manga))
+            }
+        } else {
+            if (selectedMangas.remove(manga)) {
+                selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
+            }
+        }
+    }
+
+    /**
+     * Move the selected manga to a list of categories.
+     */
+    private fun showChangeMangaCategoriesDialog() {
+        // Create a copy of selected manga
+        val mangas = selectedMangas.toList()
+
+        // Hide the default category because it has a different behavior than the ones from db.
+        val categories = presenter.categories.filter { it.id != 0 }
+
+        // Get indexes of the common categories to preselect.
+        val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
+                .map { categories.indexOf(it) }
+                .toTypedArray()
+
+        ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
+                .showDialog(router)
+    }
+
+    private fun showDeleteMangaDialog() {
+        DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
+    }
+
+    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
+        presenter.moveMangasToCategories(categories, mangas)
+        destroyActionModeIfNeeded()
+    }
+
+    override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
+        presenter.removeMangaFromLibrary(mangas, deleteChapters)
+        destroyActionModeIfNeeded()
+    }
+
+    /**
+     * Changes the cover for the selected manga.
+     */
+    private fun changeSelectedCover() {
+        val manga = selectedMangas.firstOrNull() ?: return
+        selectedCoverManga = manga
+
+        if (manga.favorite) {
+            val intent = Intent(Intent.ACTION_GET_CONTENT)
+            intent.type = "image/*"
+            startActivityForResult(Intent.createChooser(intent,
+                    resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
+        } else {
+            activity?.toast(R.string.notification_first_add_to_library)
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_IMAGE_OPEN) {
+            if (data == null || resultCode != Activity.RESULT_OK) return
+            val activity = activity ?: return
+            val manga = selectedCoverManga ?: return
+
+            try {
+                // Get the file's input stream from the incoming Intent
+                activity.contentResolver.openInputStream(data.data).use {
+                    // Update cover to selected file, show error if something went wrong
+                    if (presenter.editCoverWithStream(it, manga)) {
+                        // TODO refresh cover
+                    } else {
+                        activity.toast(R.string.notification_cover_update_failed)
+                    }
+                }
+            } catch (error: IOException) {
+                activity.toast(R.string.notification_cover_update_failed)
+                Timber.e(error)
+            }
+            selectedCoverManga = null
+        }
+    }
+
+    private companion object {
+        /**
+         * Key to change the cover of a manga in [onActivityResult].
+         */
+        const val REQUEST_IMAGE_OPEN = 101
+    }
+
+}

+ 57 - 57
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt

@@ -1,57 +1,57 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.source.LocalSource
-import kotlinx.android.synthetic.main.catalogue_grid_item.*
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_catalogue_grid" are available in this class.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-class LibraryGridHolder(
-        private val view: View,
-        private val adapter: FlexibleAdapter<*>
-
-) : LibraryHolder(view, adapter) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    override fun onSetValues(item: LibraryItem) {
-        // Update the title of the manga.
-        title.text = item.manga.title
-
-        // Update the unread count and its visibility.
-        with(unread_text) {
-            visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
-            text = item.manga.unread.toString()
-        }
-        // Update the download count and its visibility.
-        with(download_text) {
-            visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
-            text = item.downloadCount.toString()
-        }
-        //set local visibility if its local manga
-        local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
-
-        // Update the cover.
-        GlideApp.with(view.context).clear(thumbnail)
-        GlideApp.with(view.context)
-                .load(item.manga)
-                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                .centerCrop()
-                .into(thumbnail)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.source.LocalSource
+import kotlinx.android.synthetic.main.catalogue_grid_item.*
+
+/**
+ * Class used to hold the displayed data of a manga in the library, like the cover or the title.
+ * All the elements from the layout file "item_catalogue_grid" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to single tap and long tap events.
+ * @constructor creates a new library holder.
+ */
+class LibraryGridHolder(
+        private val view: View,
+        private val adapter: FlexibleAdapter<*>
+
+) : LibraryHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param item the manga item to bind.
+     */
+    override fun onSetValues(item: LibraryItem) {
+        // Update the title of the manga.
+        title.text = item.manga.title
+
+        // Update the unread count and its visibility.
+        with(unread_text) {
+            visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
+            text = item.manga.unread.toString()
+        }
+        // Update the download count and its visibility.
+        with(download_text) {
+            visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
+            text = item.downloadCount.toString()
+        }
+        //set local visibility if its local manga
+        local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
+
+        // Update the cover.
+        GlideApp.with(view.context).clear(thumbnail)
+        GlideApp.with(view.context)
+                .load(item.manga)
+                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                .centerCrop()
+                .into(thumbnail)
+    }
+
+}

+ 27 - 27
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt

@@ -1,27 +1,27 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
-
-/**
- * Generic class used to hold the displayed data of a manga in the library.
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to the single tap and long tap events.
- */
-
-abstract class LibraryHolder(
-        view: View,
-        adapter: FlexibleAdapter<*>
-) : BaseFlexibleViewHolder(view, adapter) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    abstract fun onSetValues(item: LibraryItem)
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
+
+/**
+ * Generic class used to hold the displayed data of a manga in the library.
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to the single tap and long tap events.
+ */
+
+abstract class LibraryHolder(
+        view: View,
+        adapter: FlexibleAdapter<*>
+) : BaseFlexibleViewHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param item the manga item to bind.
+     */
+    abstract fun onSetValues(item: LibraryItem)
+
+}

+ 72 - 72
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt

@@ -1,73 +1,73 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.widget.FrameLayout
-import com.f2prateek.rx.preferences.Preference
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFilterable
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.LibraryManga
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
-
-class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
-        AbstractFlexibleItem<LibraryHolder>(), IFilterable {
-
-    var downloadCount = -1
-
-    override fun getLayoutRes(): Int {
-        return if (libraryAsList.getOrDefault())
-            R.layout.catalogue_list_item
-        else
-            R.layout.catalogue_grid_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder {
-        val parent = adapter.recyclerView
-        return if (parent is AutofitRecyclerView) {
-            view.apply {
-                val coverHeight = parent.itemWidth / 3 * 4
-                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
-                gradient.layoutParams = FrameLayout.LayoutParams(
-                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
-            }
-            LibraryGridHolder(view, adapter)
-        } else {
-            LibraryListHolder(view, adapter)
-        }
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
-                                holder: LibraryHolder,
-                                position: Int,
-                                payloads: List<Any?>?) {
-
-        holder.onSetValues(this)
-    }
-
-    /**
-     * Filters a manga depending on a query.
-     *
-     * @param constraint the query to apply.
-     * @return true if the manga should be included, false otherwise.
-     */
-    override fun filter(constraint: String): Boolean {
-        return manga.title.contains(constraint, true) ||
-                (manga.author?.contains(constraint, true) ?: false)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (other is LibraryItem) {
-            return manga.id == other.manga.id
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return manga.id!!.hashCode()
-    }
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import com.f2prateek.rx.preferences.Preference
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFilterable
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.widget.AutofitRecyclerView
+import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
+
+class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
+        AbstractFlexibleItem<LibraryHolder>(), IFilterable {
+
+    var downloadCount = -1
+
+    override fun getLayoutRes(): Int {
+        return if (libraryAsList.getOrDefault())
+            R.layout.catalogue_list_item
+        else
+            R.layout.catalogue_grid_item
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder {
+        val parent = adapter.recyclerView
+        return if (parent is AutofitRecyclerView) {
+            view.apply {
+                val coverHeight = parent.itemWidth / 3 * 4
+                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
+                gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
+            }
+            LibraryGridHolder(view, adapter)
+        } else {
+            LibraryListHolder(view, adapter)
+        }
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: LibraryHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
+        holder.onSetValues(this)
+    }
+
+    /**
+     * Filters a manga depending on a query.
+     *
+     * @param constraint the query to apply.
+     * @return true if the manga should be included, false otherwise.
+     */
+    override fun filter(constraint: String): Boolean {
+        return manga.title.contains(constraint, true) ||
+                (manga.author?.contains(constraint, true) ?: false)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other is LibraryItem) {
+            return manga.id == other.manga.id
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return manga.id!!.hashCode()
+    }
 }

+ 65 - 65
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt

@@ -1,65 +1,65 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.source.LocalSource
-import kotlinx.android.synthetic.main.catalogue_list_item.*
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_library_list" are available in this class.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-
-class LibraryListHolder(
-        private val view: View,
-        private val adapter: FlexibleAdapter<*>
-) : LibraryHolder(view, adapter) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param item the manga item to bind.
-     */
-    override fun onSetValues(item: LibraryItem) {
-        // Update the title of the manga.
-        title.text = item.manga.title
-
-        // Update the unread count and its visibility.
-        with(unread_text) {
-            visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
-            text = item.manga.unread.toString()
-        }
-        // Update the download count and its visibility.
-        with(download_text) {
-            visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
-            text = "${item.downloadCount}"
-        }
-        //show local text badge if local manga
-        local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
-
-        // Create thumbnail onclick to simulate long click
-        thumbnail.setOnClickListener {
-            // Simulate long click on this view to enter selection mode
-            onLongClick(itemView)
-        }
-
-        // Update the cover.
-        GlideApp.with(itemView.context).clear(thumbnail)
-        GlideApp.with(itemView.context)
-                .load(item.manga)
-                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                .centerCrop()
-                .circleCrop()
-                .dontAnimate()
-                .into(thumbnail)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.source.LocalSource
+import kotlinx.android.synthetic.main.catalogue_list_item.*
+
+/**
+ * Class used to hold the displayed data of a manga in the library, like the cover or the title.
+ * All the elements from the layout file "item_library_list" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to single tap and long tap events.
+ * @constructor creates a new library holder.
+ */
+
+class LibraryListHolder(
+        private val view: View,
+        private val adapter: FlexibleAdapter<*>
+) : LibraryHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param item the manga item to bind.
+     */
+    override fun onSetValues(item: LibraryItem) {
+        // Update the title of the manga.
+        title.text = item.manga.title
+
+        // Update the unread count and its visibility.
+        with(unread_text) {
+            visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
+            text = item.manga.unread.toString()
+        }
+        // Update the download count and its visibility.
+        with(download_text) {
+            visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
+            text = "${item.downloadCount}"
+        }
+        //show local text badge if local manga
+        local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
+
+        // Create thumbnail onclick to simulate long click
+        thumbnail.setOnClickListener {
+            // Simulate long click on this view to enter selection mode
+            onLongClick(itemView)
+        }
+
+        // Update the cover.
+        GlideApp.with(itemView.context).clear(thumbnail)
+        GlideApp.with(itemView.context)
+                .load(item.manga)
+                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                .centerCrop()
+                .circleCrop()
+                .dontAnimate()
+                .into(thumbnail)
+    }
+
+}

+ 216 - 216
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt

@@ -1,217 +1,217 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.content.Context
-import android.util.AttributeSet
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
-import uy.kohesive.injekt.injectLazy
-
-/**
- * The navigation view shown in a drawer with the different options to show the library.
- */
-class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
-    : ExtendedNavigationView(context, attrs) {
-
-    /**
-     * Preferences helper.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * List of groups shown in the view.
-     */
-    private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup())
-
-    /**
-     * Adapter instance.
-     */
-    private val adapter = Adapter(groups.map { it.createItems() }.flatten())
-
-    /**
-     * Click listener to notify the parent fragment when an item from a group is clicked.
-     */
-    var onGroupClicked: (Group) -> Unit = {}
-
-    init {
-        recycler.adapter = adapter
-        addView(recycler)
-
-        groups.forEach { it.initModels() }
-    }
-
-    /**
-     * Returns true if there's at least one filter from [FilterGroup] active.
-     */
-    fun hasActiveFilters(): Boolean {
-        return (groups[0] as FilterGroup).items.any { it.checked }
-    }
-
-    /**
-     * Adapter of the recycler view.
-     */
-    inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
-
-        override fun onItemClicked(item: Item) {
-            if (item is GroupedItem) {
-                item.group.onItemClicked(item)
-                onGroupClicked(item.group)
-            }
-        }
-    }
-
-    /**
-     * Filters group (unread, downloaded, ...).
-     */
-    inner class FilterGroup : Group {
-
-        private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
-
-        private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
-
-        private val completed = Item.CheckboxGroup(R.string.completed, this)
-
-        override val items = listOf(downloaded, unread, completed)
-
-        override val header = Item.Header(R.string.action_filter)
-
-        override val footer = Item.Separator()
-
-        override fun initModels() {
-            downloaded.checked = preferences.filterDownloaded().getOrDefault()
-            unread.checked = preferences.filterUnread().getOrDefault()
-            completed.checked = preferences.filterCompleted().getOrDefault()
-        }
-
-        override fun onItemClicked(item: Item) {
-            item as Item.CheckboxGroup
-            item.checked = !item.checked
-            when (item) {
-                downloaded -> preferences.filterDownloaded().set(item.checked)
-                unread -> preferences.filterUnread().set(item.checked)
-                completed -> preferences.filterCompleted().set(item.checked)
-            }
-
-            adapter.notifyItemChanged(item)
-        }
-    }
-
-    /**
-     * Sorting group (alphabetically, by last read, ...) and ascending or descending.
-     */
-    inner class SortGroup : Group {
-
-        private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
-
-        private val total = Item.MultiSort(R.string.action_sort_total, this)
-
-        private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
-
-        private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
-
-        private val unread = Item.MultiSort(R.string.action_filter_unread, this)
-
-        private val source = Item.MultiSort(R.string.manga_info_source_label, this)
-
-        override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
-
-        override val header = Item.Header(R.string.action_sort)
-
-        override val footer = Item.Separator()
-
-        override fun initModels() {
-            val sorting = preferences.librarySortingMode().getOrDefault()
-            val order = if (preferences.librarySortingAscending().getOrDefault())
-                SORT_ASC else SORT_DESC
-
-            alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
-            lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
-            lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
-            unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
-            total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
-            source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE
-        }
-
-        override fun onItemClicked(item: Item) {
-            item as Item.MultiStateGroup
-            val prevState = item.state
-
-            item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
-            item.state = when (prevState) {
-                SORT_NONE -> SORT_ASC
-                SORT_ASC -> SORT_DESC
-                SORT_DESC -> SORT_ASC
-                else -> throw Exception("Unknown state")
-            }
-
-            preferences.librarySortingMode().set(when (item) {
-                alphabetically -> LibrarySort.ALPHA
-                lastRead -> LibrarySort.LAST_READ
-                lastUpdated -> LibrarySort.LAST_UPDATED
-                unread -> LibrarySort.UNREAD
-                total -> LibrarySort.TOTAL
-                source -> LibrarySort.SOURCE
-                else -> throw Exception("Unknown sorting")
-            })
-            preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
-
-            item.group.items.forEach { adapter.notifyItemChanged(it) }
-        }
-
-    }
-
-    inner class BadgeGroup : Group {
-        private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
-        override val header = null
-        override val footer = null
-        override val items = listOf(downloadBadge)
-        override fun initModels() {
-            downloadBadge.checked = preferences.downloadBadge().getOrDefault()
-        }
-
-        override fun onItemClicked(item: Item) {
-            item as Item.CheckboxGroup
-            item.checked = !item.checked
-            preferences.downloadBadge().set((item.checked))
-            adapter.notifyItemChanged(item)
-        }
-    }
-
-    /**
-     * Display group, to show the library as a list or a grid.
-     */
-    inner class DisplayGroup : Group {
-
-        private val grid = Item.Radio(R.string.action_display_grid, this)
-
-        private val list = Item.Radio(R.string.action_display_list, this)
-
-        override val items = listOf(grid, list)
-
-        override val header = Item.Header(R.string.action_display)
-
-        override val footer = null
-
-        override fun initModels() {
-            val asList = preferences.libraryAsList().getOrDefault()
-            grid.checked = !asList
-            list.checked = asList
-        }
-
-        override fun onItemClicked(item: Item) {
-            item as Item.Radio
-            if (item.checked) return
-
-            item.group.items.forEach { (it as Item.Radio).checked = false }
-            item.checked = true
-
-            preferences.libraryAsList().set(if (item == list) true else false)
-
-            item.group.items.forEach { adapter.notifyItemChanged(it) }
-        }
-    }
+package eu.kanade.tachiyomi.ui.library
+
+import android.content.Context
+import android.util.AttributeSet
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.widget.ExtendedNavigationView
+import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
+import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
+import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * The navigation view shown in a drawer with the different options to show the library.
+ */
+class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
+    : ExtendedNavigationView(context, attrs) {
+
+    /**
+     * Preferences helper.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    /**
+     * List of groups shown in the view.
+     */
+    private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup())
+
+    /**
+     * Adapter instance.
+     */
+    private val adapter = Adapter(groups.map { it.createItems() }.flatten())
+
+    /**
+     * Click listener to notify the parent fragment when an item from a group is clicked.
+     */
+    var onGroupClicked: (Group) -> Unit = {}
+
+    init {
+        recycler.adapter = adapter
+        addView(recycler)
+
+        groups.forEach { it.initModels() }
+    }
+
+    /**
+     * Returns true if there's at least one filter from [FilterGroup] active.
+     */
+    fun hasActiveFilters(): Boolean {
+        return (groups[0] as FilterGroup).items.any { it.checked }
+    }
+
+    /**
+     * Adapter of the recycler view.
+     */
+    inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
+
+        override fun onItemClicked(item: Item) {
+            if (item is GroupedItem) {
+                item.group.onItemClicked(item)
+                onGroupClicked(item.group)
+            }
+        }
+    }
+
+    /**
+     * Filters group (unread, downloaded, ...).
+     */
+    inner class FilterGroup : Group {
+
+        private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
+
+        private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
+
+        private val completed = Item.CheckboxGroup(R.string.completed, this)
+
+        override val items = listOf(downloaded, unread, completed)
+
+        override val header = Item.Header(R.string.action_filter)
+
+        override val footer = Item.Separator()
+
+        override fun initModels() {
+            downloaded.checked = preferences.filterDownloaded().getOrDefault()
+            unread.checked = preferences.filterUnread().getOrDefault()
+            completed.checked = preferences.filterCompleted().getOrDefault()
+        }
+
+        override fun onItemClicked(item: Item) {
+            item as Item.CheckboxGroup
+            item.checked = !item.checked
+            when (item) {
+                downloaded -> preferences.filterDownloaded().set(item.checked)
+                unread -> preferences.filterUnread().set(item.checked)
+                completed -> preferences.filterCompleted().set(item.checked)
+            }
+
+            adapter.notifyItemChanged(item)
+        }
+    }
+
+    /**
+     * Sorting group (alphabetically, by last read, ...) and ascending or descending.
+     */
+    inner class SortGroup : Group {
+
+        private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
+
+        private val total = Item.MultiSort(R.string.action_sort_total, this)
+
+        private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
+
+        private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
+
+        private val unread = Item.MultiSort(R.string.action_filter_unread, this)
+
+        private val source = Item.MultiSort(R.string.manga_info_source_label, this)
+
+        override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
+
+        override val header = Item.Header(R.string.action_sort)
+
+        override val footer = Item.Separator()
+
+        override fun initModels() {
+            val sorting = preferences.librarySortingMode().getOrDefault()
+            val order = if (preferences.librarySortingAscending().getOrDefault())
+                SORT_ASC else SORT_DESC
+
+            alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
+            lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
+            lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
+            unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
+            total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
+            source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE
+        }
+
+        override fun onItemClicked(item: Item) {
+            item as Item.MultiStateGroup
+            val prevState = item.state
+
+            item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
+            item.state = when (prevState) {
+                SORT_NONE -> SORT_ASC
+                SORT_ASC -> SORT_DESC
+                SORT_DESC -> SORT_ASC
+                else -> throw Exception("Unknown state")
+            }
+
+            preferences.librarySortingMode().set(when (item) {
+                alphabetically -> LibrarySort.ALPHA
+                lastRead -> LibrarySort.LAST_READ
+                lastUpdated -> LibrarySort.LAST_UPDATED
+                unread -> LibrarySort.UNREAD
+                total -> LibrarySort.TOTAL
+                source -> LibrarySort.SOURCE
+                else -> throw Exception("Unknown sorting")
+            })
+            preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
+
+            item.group.items.forEach { adapter.notifyItemChanged(it) }
+        }
+
+    }
+
+    inner class BadgeGroup : Group {
+        private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
+        override val header = null
+        override val footer = null
+        override val items = listOf(downloadBadge)
+        override fun initModels() {
+            downloadBadge.checked = preferences.downloadBadge().getOrDefault()
+        }
+
+        override fun onItemClicked(item: Item) {
+            item as Item.CheckboxGroup
+            item.checked = !item.checked
+            preferences.downloadBadge().set((item.checked))
+            adapter.notifyItemChanged(item)
+        }
+    }
+
+    /**
+     * Display group, to show the library as a list or a grid.
+     */
+    inner class DisplayGroup : Group {
+
+        private val grid = Item.Radio(R.string.action_display_grid, this)
+
+        private val list = Item.Radio(R.string.action_display_list, this)
+
+        override val items = listOf(grid, list)
+
+        override val header = Item.Header(R.string.action_display)
+
+        override val footer = null
+
+        override fun initModels() {
+            val asList = preferences.libraryAsList().getOrDefault()
+            grid.checked = !asList
+            list.checked = asList
+        }
+
+        override fun onItemClicked(item: Item) {
+            item as Item.Radio
+            if (item.checked) return
+
+            item.group.items.forEach { (it as Item.Radio).checked = false }
+            item.checked = true
+
+            preferences.libraryAsList().set(if (item == list) true else false)
+
+            item.group.items.forEach { adapter.notifyItemChanged(it) }
+        }
+    }
 }

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

@@ -1,371 +1,371 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.os.Bundle
-import com.jakewharton.rxrelay.BehaviorRelay
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.util.combineLatest
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.IOException
-import java.io.InputStream
-import java.util.ArrayList
-import java.util.Collections
-import java.util.Comparator
-
-/**
- * Class containing library information.
- */
-private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
-
-/**
- * Typealias for the library manga, using the category as keys, and list of manga as values.
- */
-private typealias LibraryMap = Map<Int, List<LibraryItem>>
-
-/**
- * Presenter of [LibraryController].
- */
-class LibraryPresenter(
-        private val db: DatabaseHelper = Injekt.get(),
-        private val preferences: PreferencesHelper = Injekt.get(),
-        private val coverCache: CoverCache = Injekt.get(),
-        private val sourceManager: SourceManager = Injekt.get(),
-        private val downloadManager: DownloadManager = Injekt.get()
-) : BasePresenter<LibraryController>() {
-
-    private val context = preferences.context
-
-    /**
-     * Categories of the library.
-     */
-    var categories: List<Category> = emptyList()
-        private set
-
-    /**
-     * Relay used to apply the UI filters to the last emission of the library.
-     */
-    private val filterTriggerRelay = BehaviorRelay.create(Unit)
-
-    /**
-     * Relay used to apply the UI update to the last emission of the library.
-     */
-    private val downloadTriggerRelay = BehaviorRelay.create(Unit)
-
-    /**
-     * Relay used to apply the selected sorting method to the last emission of the library.
-     */
-    private val sortTriggerRelay = BehaviorRelay.create(Unit)
-
-    /**
-     * Library subscription.
-     */
-    private var librarySubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        subscribeLibrary()
-    }
-
-    /**
-     * Subscribes to library if needed.
-     */
-    fun subscribeLibrary() {
-        if (librarySubscription.isNullOrUnsubscribed()) {
-            librarySubscription = getLibraryObservable()
-                    .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()),
-                            { lib, _ -> lib.apply { setDownloadCount(mangaMap) } })
-                    .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
-                            { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) })
-                    .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
-                            { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) })
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribeLatestCache({ view, (categories, mangaMap) ->
-                        view.onNextLibraryUpdate(categories, mangaMap)
-                    })
-        }
-    }
-
-    /**
-     * Applies library filters to the given map of manga.
-     *
-     * @param map the map to filter.
-     */
-    private fun applyFilters(map: LibraryMap): LibraryMap {
-        val filterDownloaded = preferences.filterDownloaded().getOrDefault()
-
-        val filterUnread = preferences.filterUnread().getOrDefault()
-
-        val filterCompleted = preferences.filterCompleted().getOrDefault()
-
-        val filterFn: (LibraryItem) -> Boolean = f@ { item ->
-            // Filter when there isn't unread chapters.
-            if (filterUnread && item.manga.unread == 0) {
-                return@f false
-            }
-
-            if (filterCompleted && item.manga.status != SManga.COMPLETED) {
-                return@f false
-            }
-
-            // Filter when there are no downloads.
-            if (filterDownloaded) {
-                // Local manga are always downloaded
-                if (item.manga.source == LocalSource.ID) {
-                    return@f true
-                }
-                // Don't bother with directory checking if download count has been set.
-                if (item.downloadCount != -1) {
-                    return@f item.downloadCount > 0
-                }
-
-                return@f downloadManager.getDownloadCount(item.manga) > 0
-            }
-            true
-        }
-
-        return map.mapValues { entry -> entry.value.filter(filterFn) }
-    }
-
-    /**
-     * Sets downloaded chapter count to each manga.
-     *
-     * @param map the map of manga.
-     */
-    private fun setDownloadCount(map: LibraryMap) {
-        if (!preferences.downloadBadge().getOrDefault()) {
-            // Unset download count if the preference is not enabled.
-            for ((_, itemList) in map) {
-                for (item in itemList) {
-                    item.downloadCount = -1
-                }
-            }
-            return
-        }
-
-        for ((_, itemList) in map) {
-            for (item in itemList) {
-                item.downloadCount = downloadManager.getDownloadCount(item.manga)
-            }
-        }
-    }
-
-    /**
-     * Applies library sorting to the given map of manga.
-     *
-     * @param map the map to sort.
-     */
-    private fun applySort(map: LibraryMap): LibraryMap {
-        val sortingMode = preferences.librarySortingMode().getOrDefault()
-
-        val lastReadManga by lazy {
-            var counter = 0
-            db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
-        }
-        val totalChapterManga by lazy {
-            var counter = 0
-            db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
-        }
-
-        val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
-            when (sortingMode) {
-                LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
-                LibrarySort.LAST_READ -> {
-                    // Get index of manga, set equal to list if size unknown.
-                    val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
-                    val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size
-                    manga1LastRead.compareTo(manga2LastRead)
-                }
-                LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update)
-                LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
-                LibrarySort.TOTAL -> {
-                    val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
-                    val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
-                    manga1TotalChapter.compareTo(mange2TotalChapter)
-                }
-                LibrarySort.SOURCE -> {
-                    val source1Name = sourceManager.getOrStub(i1.manga.source).name
-                    val source2Name = sourceManager.getOrStub(i2.manga.source).name
-                    source1Name.compareTo(source2Name)
-                }
-                else -> throw Exception("Unknown sorting mode")
-            }
-        }
-
-        val comparator = if (preferences.librarySortingAscending().getOrDefault())
-            Comparator(sortFn)
-        else
-            Collections.reverseOrder(sortFn)
-
-        return map.mapValues { entry -> entry.value.sortedWith(comparator) }
-    }
-
-    /**
-     * Get the categories and all its manga from the database.
-     *
-     * @return an observable of the categories and its manga.
-     */
-    private fun getLibraryObservable(): Observable<Library> {
-        return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
-                { dbCategories, libraryManga ->
-                    val categories = if (libraryManga.containsKey(0))
-                        arrayListOf(Category.createDefault()) + dbCategories
-                    else
-                        dbCategories
-
-                    this.categories = categories
-                    Library(categories, libraryManga)
-                })
-    }
-
-    /**
-     * Get the categories from the database.
-     *
-     * @return an observable of the categories.
-     */
-    private fun getCategoriesObservable(): Observable<List<Category>> {
-        return db.getCategories().asRxObservable()
-    }
-
-    /**
-     * Get the manga grouped by categories.
-     *
-     * @return an observable containing a map with the category id as key and a list of manga as the
-     * value.
-     */
-    private fun getLibraryMangasObservable(): Observable<LibraryMap> {
-        val libraryAsList = preferences.libraryAsList()
-        return db.getLibraryMangas().asRxObservable()
-                .map { list ->
-                    list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category }
-                }
-    }
-
-    /**
-     * Requests the library to be filtered.
-     */
-    fun requestFilterUpdate() {
-        filterTriggerRelay.call(Unit)
-    }
-
-    /**
-     * Requests the library to have download badges added.
-     */
-    fun requestDownloadBadgesUpdate() {
-        downloadTriggerRelay.call(Unit)
-    }
-
-    /**
-     * Requests the library to be sorted.
-     */
-    fun requestSortUpdate() {
-        sortTriggerRelay.call(Unit)
-    }
-
-    /**
-     * Called when a manga is opened.
-     */
-    fun onOpenManga() {
-        // Avoid further db updates for the library when it's not needed
-        librarySubscription?.let { remove(it) }
-    }
-
-    /**
-     * Returns the common categories for the given list of manga.
-     *
-     * @param mangas the list of manga.
-     */
-    fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
-        if (mangas.isEmpty()) return emptyList()
-        return mangas.toSet()
-                .map { db.getCategoriesForManga(it).executeAsBlocking() }
-                .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
-    }
-
-    /**
-     * Remove the selected manga from the library.
-     *
-     * @param mangas the list of manga to delete.
-     * @param deleteChapters whether to also delete downloaded chapters.
-     */
-    fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
-        // Create a set of the list
-        val mangaToDelete = mangas.distinctBy { it.id }
-        mangaToDelete.forEach { it.favorite = false }
-
-        Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
-                .onErrorResumeNext { Observable.empty() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-
-        Observable.fromCallable {
-            mangaToDelete.forEach { manga ->
-                coverCache.deleteFromCache(manga.thumbnail_url)
-                if (deleteChapters) {
-                    val source = sourceManager.get(manga.source) as? HttpSource
-                    if (source != null) {
-                        downloadManager.deleteManga(manga, source)
-                    }
-                }
-            }
-        }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Move the given list of manga to categories.
-     *
-     * @param categories the selected categories.
-     * @param mangas the list of manga to move.
-     */
-    fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
-        val mc = ArrayList<MangaCategory>()
-
-        for (manga in mangas) {
-            for (cat in categories) {
-                mc.add(MangaCategory.create(manga, cat))
-            }
-        }
-
-        db.setMangaCategories(mc, mangas)
-    }
-
-    /**
-     * Update cover with local file.
-     *
-     * @param inputStream the new cover.
-     * @param manga the manga edited.
-     * @return true if the cover is updated, false otherwise
-     */
-    @Throws(IOException::class)
-    fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
-        if (manga.source == LocalSource.ID) {
-            LocalSource.updateCover(context, manga, inputStream)
-            return true
-        }
-
-        if (manga.thumbnail_url != null && manga.favorite) {
-            coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
-            return true
-        }
-        return false
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.os.Bundle
+import com.jakewharton.rxrelay.BehaviorRelay
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaCategory
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.combineLatest
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.IOException
+import java.io.InputStream
+import java.util.ArrayList
+import java.util.Collections
+import java.util.Comparator
+
+/**
+ * Class containing library information.
+ */
+private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
+
+/**
+ * Typealias for the library manga, using the category as keys, and list of manga as values.
+ */
+private typealias LibraryMap = Map<Int, List<LibraryItem>>
+
+/**
+ * Presenter of [LibraryController].
+ */
+class LibraryPresenter(
+        private val db: DatabaseHelper = Injekt.get(),
+        private val preferences: PreferencesHelper = Injekt.get(),
+        private val coverCache: CoverCache = Injekt.get(),
+        private val sourceManager: SourceManager = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get()
+) : BasePresenter<LibraryController>() {
+
+    private val context = preferences.context
+
+    /**
+     * Categories of the library.
+     */
+    var categories: List<Category> = emptyList()
+        private set
+
+    /**
+     * Relay used to apply the UI filters to the last emission of the library.
+     */
+    private val filterTriggerRelay = BehaviorRelay.create(Unit)
+
+    /**
+     * Relay used to apply the UI update to the last emission of the library.
+     */
+    private val downloadTriggerRelay = BehaviorRelay.create(Unit)
+
+    /**
+     * Relay used to apply the selected sorting method to the last emission of the library.
+     */
+    private val sortTriggerRelay = BehaviorRelay.create(Unit)
+
+    /**
+     * Library subscription.
+     */
+    private var librarySubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        subscribeLibrary()
+    }
+
+    /**
+     * Subscribes to library if needed.
+     */
+    fun subscribeLibrary() {
+        if (librarySubscription.isNullOrUnsubscribed()) {
+            librarySubscription = getLibraryObservable()
+                    .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()),
+                            { lib, _ -> lib.apply { setDownloadCount(mangaMap) } })
+                    .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
+                            { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) })
+                    .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
+                            { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) })
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribeLatestCache({ view, (categories, mangaMap) ->
+                        view.onNextLibraryUpdate(categories, mangaMap)
+                    })
+        }
+    }
+
+    /**
+     * Applies library filters to the given map of manga.
+     *
+     * @param map the map to filter.
+     */
+    private fun applyFilters(map: LibraryMap): LibraryMap {
+        val filterDownloaded = preferences.filterDownloaded().getOrDefault()
+
+        val filterUnread = preferences.filterUnread().getOrDefault()
+
+        val filterCompleted = preferences.filterCompleted().getOrDefault()
+
+        val filterFn: (LibraryItem) -> Boolean = f@ { item ->
+            // Filter when there isn't unread chapters.
+            if (filterUnread && item.manga.unread == 0) {
+                return@f false
+            }
+
+            if (filterCompleted && item.manga.status != SManga.COMPLETED) {
+                return@f false
+            }
+
+            // Filter when there are no downloads.
+            if (filterDownloaded) {
+                // Local manga are always downloaded
+                if (item.manga.source == LocalSource.ID) {
+                    return@f true
+                }
+                // Don't bother with directory checking if download count has been set.
+                if (item.downloadCount != -1) {
+                    return@f item.downloadCount > 0
+                }
+
+                return@f downloadManager.getDownloadCount(item.manga) > 0
+            }
+            true
+        }
+
+        return map.mapValues { entry -> entry.value.filter(filterFn) }
+    }
+
+    /**
+     * Sets downloaded chapter count to each manga.
+     *
+     * @param map the map of manga.
+     */
+    private fun setDownloadCount(map: LibraryMap) {
+        if (!preferences.downloadBadge().getOrDefault()) {
+            // Unset download count if the preference is not enabled.
+            for ((_, itemList) in map) {
+                for (item in itemList) {
+                    item.downloadCount = -1
+                }
+            }
+            return
+        }
+
+        for ((_, itemList) in map) {
+            for (item in itemList) {
+                item.downloadCount = downloadManager.getDownloadCount(item.manga)
+            }
+        }
+    }
+
+    /**
+     * Applies library sorting to the given map of manga.
+     *
+     * @param map the map to sort.
+     */
+    private fun applySort(map: LibraryMap): LibraryMap {
+        val sortingMode = preferences.librarySortingMode().getOrDefault()
+
+        val lastReadManga by lazy {
+            var counter = 0
+            db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
+        }
+        val totalChapterManga by lazy {
+            var counter = 0
+            db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
+        }
+
+        val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
+            when (sortingMode) {
+                LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
+                LibrarySort.LAST_READ -> {
+                    // Get index of manga, set equal to list if size unknown.
+                    val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
+                    val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size
+                    manga1LastRead.compareTo(manga2LastRead)
+                }
+                LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update)
+                LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
+                LibrarySort.TOTAL -> {
+                    val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
+                    val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
+                    manga1TotalChapter.compareTo(mange2TotalChapter)
+                }
+                LibrarySort.SOURCE -> {
+                    val source1Name = sourceManager.getOrStub(i1.manga.source).name
+                    val source2Name = sourceManager.getOrStub(i2.manga.source).name
+                    source1Name.compareTo(source2Name)
+                }
+                else -> throw Exception("Unknown sorting mode")
+            }
+        }
+
+        val comparator = if (preferences.librarySortingAscending().getOrDefault())
+            Comparator(sortFn)
+        else
+            Collections.reverseOrder(sortFn)
+
+        return map.mapValues { entry -> entry.value.sortedWith(comparator) }
+    }
+
+    /**
+     * Get the categories and all its manga from the database.
+     *
+     * @return an observable of the categories and its manga.
+     */
+    private fun getLibraryObservable(): Observable<Library> {
+        return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
+                { dbCategories, libraryManga ->
+                    val categories = if (libraryManga.containsKey(0))
+                        arrayListOf(Category.createDefault()) + dbCategories
+                    else
+                        dbCategories
+
+                    this.categories = categories
+                    Library(categories, libraryManga)
+                })
+    }
+
+    /**
+     * Get the categories from the database.
+     *
+     * @return an observable of the categories.
+     */
+    private fun getCategoriesObservable(): Observable<List<Category>> {
+        return db.getCategories().asRxObservable()
+    }
+
+    /**
+     * Get the manga grouped by categories.
+     *
+     * @return an observable containing a map with the category id as key and a list of manga as the
+     * value.
+     */
+    private fun getLibraryMangasObservable(): Observable<LibraryMap> {
+        val libraryAsList = preferences.libraryAsList()
+        return db.getLibraryMangas().asRxObservable()
+                .map { list ->
+                    list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category }
+                }
+    }
+
+    /**
+     * Requests the library to be filtered.
+     */
+    fun requestFilterUpdate() {
+        filterTriggerRelay.call(Unit)
+    }
+
+    /**
+     * Requests the library to have download badges added.
+     */
+    fun requestDownloadBadgesUpdate() {
+        downloadTriggerRelay.call(Unit)
+    }
+
+    /**
+     * Requests the library to be sorted.
+     */
+    fun requestSortUpdate() {
+        sortTriggerRelay.call(Unit)
+    }
+
+    /**
+     * Called when a manga is opened.
+     */
+    fun onOpenManga() {
+        // Avoid further db updates for the library when it's not needed
+        librarySubscription?.let { remove(it) }
+    }
+
+    /**
+     * Returns the common categories for the given list of manga.
+     *
+     * @param mangas the list of manga.
+     */
+    fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
+        if (mangas.isEmpty()) return emptyList()
+        return mangas.toSet()
+                .map { db.getCategoriesForManga(it).executeAsBlocking() }
+                .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
+    }
+
+    /**
+     * Remove the selected manga from the library.
+     *
+     * @param mangas the list of manga to delete.
+     * @param deleteChapters whether to also delete downloaded chapters.
+     */
+    fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
+        // Create a set of the list
+        val mangaToDelete = mangas.distinctBy { it.id }
+        mangaToDelete.forEach { it.favorite = false }
+
+        Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
+                .onErrorResumeNext { Observable.empty() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+
+        Observable.fromCallable {
+            mangaToDelete.forEach { manga ->
+                coverCache.deleteFromCache(manga.thumbnail_url)
+                if (deleteChapters) {
+                    val source = sourceManager.get(manga.source) as? HttpSource
+                    if (source != null) {
+                        downloadManager.deleteManga(manga, source)
+                    }
+                }
+            }
+        }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Move the given list of manga to categories.
+     *
+     * @param categories the selected categories.
+     * @param mangas the list of manga to move.
+     */
+    fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
+        val mc = ArrayList<MangaCategory>()
+
+        for (manga in mangas) {
+            for (cat in categories) {
+                mc.add(MangaCategory.create(manga, cat))
+            }
+        }
+
+        db.setMangaCategories(mc, mangas)
+    }
+
+    /**
+     * Update cover with local file.
+     *
+     * @param inputStream the new cover.
+     * @param manga the manga edited.
+     * @return true if the cover is updated, false otherwise
+     */
+    @Throws(IOException::class)
+    fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
+        if (manga.source == LocalSource.ID) {
+            LocalSource.updateCover(context, manga, inputStream)
+            return true
+        }
+
+        if (manga.thumbnail_url != null && manga.favorite) {
+            coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
+            return true
+        }
+        return false
+    }
+
+}

+ 10 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt

@@ -1,11 +1,11 @@
-package eu.kanade.tachiyomi.ui.library
-
-object LibrarySort {
-
-    const val ALPHA = 0
-    const val LAST_READ = 1
-    const val LAST_UPDATED = 2
-    const val UNREAD = 3
-    const val TOTAL = 4
-    const val SOURCE = 5
+package eu.kanade.tachiyomi.ui.library
+
+object LibrarySort {
+
+    const val ALPHA = 0
+    const val LAST_READ = 1
+    const val LAST_UPDATED = 2
+    const val UNREAD = 3
+    const val TOTAL = 4
+    const val SOURCE = 5
 }

+ 31 - 31
app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt

@@ -1,32 +1,32 @@
-package eu.kanade.tachiyomi.ui.main
-
-import android.app.Dialog
-import android.content.Context
-import android.os.Bundle
-import android.util.AttributeSet
-import com.afollestad.materialdialogs.MaterialDialog
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
-
-class ChangelogDialogController : DialogController() {
-
-    override fun onCreateDialog(savedState: Bundle?): Dialog {
-        val activity = activity!!
-        val view = WhatsNewRecyclerView(activity)
-        return MaterialDialog.Builder(activity)
-                .title(if (BuildConfig.DEBUG) "Notices" else "Changelog")
-                .customView(view, false)
-                .positiveText(android.R.string.yes)
-                .build()
-    }
-
-    class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) {
-        override fun initAttrs(attrs: AttributeSet?, defStyle: Int) {
-            mRowLayoutId = R.layout.changelog_row_layout
-            mRowHeaderLayoutId = R.layout.changelog_header_layout
-            mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release
-        }
-    }
+package eu.kanade.tachiyomi.ui.main
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.kanade.tachiyomi.BuildConfig
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
+
+class ChangelogDialogController : DialogController() {
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        val activity = activity!!
+        val view = WhatsNewRecyclerView(activity)
+        return MaterialDialog.Builder(activity)
+                .title(if (BuildConfig.DEBUG) "Notices" else "Changelog")
+                .customView(view, false)
+                .positiveText(android.R.string.yes)
+                .build()
+    }
+
+    class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) {
+        override fun initAttrs(attrs: AttributeSet?, defStyle: Int) {
+            mRowLayoutId = R.layout.changelog_row_layout
+            mRowHeaderLayoutId = R.layout.changelog_header_layout
+            mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release
+        }
+    }
 }

+ 282 - 282
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -1,282 +1,282 @@
-package eu.kanade.tachiyomi.ui.main
-
-import android.animation.ObjectAnimator
-import android.app.SearchManager
-import android.content.Intent
-import android.graphics.Color
-import android.os.Bundle
-import android.support.v4.view.GravityCompat
-import android.support.v4.widget.DrawerLayout
-import android.support.v7.graphics.drawable.DrawerArrowDrawable
-import android.view.ViewGroup
-import com.bluelinelabs.conductor.*
-import eu.kanade.tachiyomi.Migrations
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
-import eu.kanade.tachiyomi.ui.base.controller.*
-import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
-import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
-import eu.kanade.tachiyomi.ui.download.DownloadController
-import eu.kanade.tachiyomi.ui.extension.ExtensionController
-import eu.kanade.tachiyomi.ui.library.LibraryController
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
-import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
-import eu.kanade.tachiyomi.ui.setting.SettingsMainController
-import eu.kanade.tachiyomi.util.openInBrowser
-import kotlinx.android.synthetic.main.main_activity.*
-import uy.kohesive.injekt.injectLazy
-
-
-class MainActivity : BaseActivity() {
-
-    private lateinit var router: Router
-
-    val preferences: PreferencesHelper by injectLazy()
-
-    private var drawerArrow: DrawerArrowDrawable? = null
-
-    private var secondaryDrawer: ViewGroup? = null
-
-    private val startScreenId by lazy {
-        when (preferences.startScreen()) {
-            2 -> R.id.nav_drawer_recently_read
-            3 -> R.id.nav_drawer_recent_updates
-            else -> R.id.nav_drawer_library
-        }
-    }
-
-    lateinit var tabAnimator: TabsAnimator
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        setTheme(when (preferences.theme()) {
-            2 -> R.style.Theme_Tachiyomi_Dark
-            3 -> R.style.Theme_Tachiyomi_Amoled
-            4 -> R.style.Theme_Tachiyomi_DarkBlue
-            else -> R.style.Theme_Tachiyomi
-        })
-        super.onCreate(savedInstanceState)
-
-        // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
-        if (!isTaskRoot) {
-            finish()
-            return
-        }
-
-        setContentView(R.layout.main_activity)
-
-        setSupportActionBar(toolbar)
-
-        drawerArrow = DrawerArrowDrawable(this)
-        drawerArrow?.color = Color.WHITE
-        toolbar.navigationIcon = drawerArrow
-
-        tabAnimator = TabsAnimator(tabs)
-
-        // Set behavior of Navigation drawer
-        nav_view.setNavigationItemSelectedListener { item ->
-            val id = item.itemId
-
-            val currentRoot = router.backstack.firstOrNull()
-            if (currentRoot?.tag()?.toIntOrNull() != id) {
-                when (id) {
-                    R.id.nav_drawer_library -> setRoot(LibraryController(), id)
-                    R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
-                    R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
-                    R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
-                    R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id)
-                    R.id.nav_drawer_downloads -> {
-                        router.pushController(DownloadController().withFadeTransaction())
-                    }
-                    R.id.nav_drawer_settings -> {
-                        router.pushController(SettingsMainController().withFadeTransaction())
-                    }
-                    R.id.nav_drawer_help -> {
-                        openInBrowser(URL_HELP)
-                    }
-                }
-            }
-            drawer.closeDrawer(GravityCompat.START)
-            true
-        }
-
-        val container: ViewGroup = findViewById(R.id.controller_container)
-
-        router = Conductor.attachRouter(this, container, savedInstanceState)
-        if (!router.hasRootController()) {
-            // Set start screen
-            if (!handleIntentAction(intent)) {
-                setSelectedDrawerItem(startScreenId)
-            }
-        }
-
-        toolbar.setNavigationOnClickListener {
-            if (router.backstackSize == 1) {
-                drawer.openDrawer(GravityCompat.START)
-            } else {
-                onBackPressed()
-            }
-        }
-
-        router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener {
-            override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean,
-                                         container: ViewGroup, handler: ControllerChangeHandler) {
-
-                syncActivityViewWithController(to, from)
-            }
-
-            override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean,
-                                           container: ViewGroup, handler: ControllerChangeHandler) {
-
-            }
-
-        })
-
-        syncActivityViewWithController(router.backstack.lastOrNull()?.controller())
-
-        if (savedInstanceState == null) {
-            // Show changelog if needed
-            if (Migrations.upgrade(preferences)) {
-                ChangelogDialogController().showDialog(router)
-            }
-        }
-    }
-
-    override fun onNewIntent(intent: Intent) {
-        if (!handleIntentAction(intent)) {
-            super.onNewIntent(intent)
-        }
-    }
-
-    private fun handleIntentAction(intent: Intent): Boolean {
-        when (intent.action) {
-            SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
-            SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
-            SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
-            SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
-            SHORTCUT_MANGA -> {
-                val extras = intent.extras ?: return false
-                router.setRoot(RouterTransaction.with(MangaController(extras)))
-            }
-            SHORTCUT_DOWNLOADS -> {
-                if (router.backstack.none { it.controller() is DownloadController }) {
-                    setSelectedDrawerItem(R.id.nav_drawer_downloads)
-                }
-            }
-            Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> {
-                //If the intent match the "standard" Android search intent
-                // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant)
-
-                //Get the search query provided in extras, and if not null, perform a global search with it.
-                val query = intent.getStringExtra(SearchManager.QUERY)
-                if (query != null && !query.isEmpty()) {
-                    if (router.backstackSize > 1) {
-                        router.popToRoot()
-                    }
-                    router.pushController(CatalogueSearchController(query).withFadeTransaction())
-                }
-            }
-            INTENT_SEARCH -> {
-                val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
-                val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
-                if (query != null && !query.isEmpty()) {
-                    if (router.backstackSize > 1) {
-                        router.popToRoot()
-                    }
-                    router.pushController(CatalogueSearchController(query, filter).withFadeTransaction())
-                }
-            }
-            else -> return false
-        }
-        return true
-    }
-
-    override fun onDestroy() {
-        super.onDestroy()
-        nav_view?.setNavigationItemSelectedListener(null)
-        toolbar?.setNavigationOnClickListener(null)
-    }
-
-    override fun onBackPressed() {
-        val backstackSize = router.backstackSize
-        if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
-            drawer.closeDrawers()
-        } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
-            setSelectedDrawerItem(startScreenId)
-        } else if (backstackSize == 1 || !router.handleBack()) {
-            super.onBackPressed()
-        }
-    }
-
-    private fun setSelectedDrawerItem(itemId: Int) {
-        if (!isFinishing) {
-            nav_view.setCheckedItem(itemId)
-            nav_view.menu.performIdentifierAction(itemId, 0)
-        }
-    }
-
-    private fun setRoot(controller: Controller, id: Int) {
-        router.setRoot(controller.withFadeTransaction().tag(id.toString()))
-    }
-
-    private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {
-        if (from is DialogController || to is DialogController) {
-            return
-        }
-
-        val showHamburger = router.backstackSize == 1
-        if (showHamburger) {
-            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
-        } else {
-            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
-        }
-
-        ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start()
-
-        if (from is TabbedController) {
-            from.cleanupTabs(tabs)
-        }
-        if (to is TabbedController) {
-            tabAnimator.expand()
-            to.configureTabs(tabs)
-        } else {
-            tabAnimator.collapse()
-            tabs.setupWithViewPager(null)
-        }
-
-        if (from is SecondaryDrawerController) {
-            if (secondaryDrawer != null) {
-                from.cleanupSecondaryDrawer(drawer)
-                drawer.removeView(secondaryDrawer)
-                secondaryDrawer = null
-            }
-        }
-        if (to is SecondaryDrawerController) {
-            secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) }
-        }
-
-        if (to is NoToolbarElevationController) {
-            appbar.disableElevation()
-        } else {
-            appbar.enableElevation()
-        }
-    }
-
-    companion object {
-        // Shortcut actions
-        const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
-        const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
-        const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
-        const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
-        const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
-        const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
-
-        const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
-        const val INTENT_SEARCH_QUERY = "query"
-        const val INTENT_SEARCH_FILTER = "filter"
-
-        private const val URL_HELP = "https://tachiyomi.org/help/"
-    }
-
-}
+package eu.kanade.tachiyomi.ui.main
+
+import android.animation.ObjectAnimator
+import android.app.SearchManager
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.support.v4.view.GravityCompat
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.graphics.drawable.DrawerArrowDrawable
+import android.view.ViewGroup
+import com.bluelinelabs.conductor.*
+import eu.kanade.tachiyomi.Migrations
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import eu.kanade.tachiyomi.ui.base.controller.*
+import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
+import eu.kanade.tachiyomi.ui.download.DownloadController
+import eu.kanade.tachiyomi.ui.extension.ExtensionController
+import eu.kanade.tachiyomi.ui.library.LibraryController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
+import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
+import eu.kanade.tachiyomi.ui.setting.SettingsMainController
+import eu.kanade.tachiyomi.util.openInBrowser
+import kotlinx.android.synthetic.main.main_activity.*
+import uy.kohesive.injekt.injectLazy
+
+
+class MainActivity : BaseActivity() {
+
+    private lateinit var router: Router
+
+    val preferences: PreferencesHelper by injectLazy()
+
+    private var drawerArrow: DrawerArrowDrawable? = null
+
+    private var secondaryDrawer: ViewGroup? = null
+
+    private val startScreenId by lazy {
+        when (preferences.startScreen()) {
+            2 -> R.id.nav_drawer_recently_read
+            3 -> R.id.nav_drawer_recent_updates
+            else -> R.id.nav_drawer_library
+        }
+    }
+
+    lateinit var tabAnimator: TabsAnimator
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setTheme(when (preferences.theme()) {
+            2 -> R.style.Theme_Tachiyomi_Dark
+            3 -> R.style.Theme_Tachiyomi_Amoled
+            4 -> R.style.Theme_Tachiyomi_DarkBlue
+            else -> R.style.Theme_Tachiyomi
+        })
+        super.onCreate(savedInstanceState)
+
+        // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
+        if (!isTaskRoot) {
+            finish()
+            return
+        }
+
+        setContentView(R.layout.main_activity)
+
+        setSupportActionBar(toolbar)
+
+        drawerArrow = DrawerArrowDrawable(this)
+        drawerArrow?.color = Color.WHITE
+        toolbar.navigationIcon = drawerArrow
+
+        tabAnimator = TabsAnimator(tabs)
+
+        // Set behavior of Navigation drawer
+        nav_view.setNavigationItemSelectedListener { item ->
+            val id = item.itemId
+
+            val currentRoot = router.backstack.firstOrNull()
+            if (currentRoot?.tag()?.toIntOrNull() != id) {
+                when (id) {
+                    R.id.nav_drawer_library -> setRoot(LibraryController(), id)
+                    R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
+                    R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
+                    R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
+                    R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id)
+                    R.id.nav_drawer_downloads -> {
+                        router.pushController(DownloadController().withFadeTransaction())
+                    }
+                    R.id.nav_drawer_settings -> {
+                        router.pushController(SettingsMainController().withFadeTransaction())
+                    }
+                    R.id.nav_drawer_help -> {
+                        openInBrowser(URL_HELP)
+                    }
+                }
+            }
+            drawer.closeDrawer(GravityCompat.START)
+            true
+        }
+
+        val container: ViewGroup = findViewById(R.id.controller_container)
+
+        router = Conductor.attachRouter(this, container, savedInstanceState)
+        if (!router.hasRootController()) {
+            // Set start screen
+            if (!handleIntentAction(intent)) {
+                setSelectedDrawerItem(startScreenId)
+            }
+        }
+
+        toolbar.setNavigationOnClickListener {
+            if (router.backstackSize == 1) {
+                drawer.openDrawer(GravityCompat.START)
+            } else {
+                onBackPressed()
+            }
+        }
+
+        router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener {
+            override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean,
+                                         container: ViewGroup, handler: ControllerChangeHandler) {
+
+                syncActivityViewWithController(to, from)
+            }
+
+            override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean,
+                                           container: ViewGroup, handler: ControllerChangeHandler) {
+
+            }
+
+        })
+
+        syncActivityViewWithController(router.backstack.lastOrNull()?.controller())
+
+        if (savedInstanceState == null) {
+            // Show changelog if needed
+            if (Migrations.upgrade(preferences)) {
+                ChangelogDialogController().showDialog(router)
+            }
+        }
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        if (!handleIntentAction(intent)) {
+            super.onNewIntent(intent)
+        }
+    }
+
+    private fun handleIntentAction(intent: Intent): Boolean {
+        when (intent.action) {
+            SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
+            SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
+            SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
+            SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
+            SHORTCUT_MANGA -> {
+                val extras = intent.extras ?: return false
+                router.setRoot(RouterTransaction.with(MangaController(extras)))
+            }
+            SHORTCUT_DOWNLOADS -> {
+                if (router.backstack.none { it.controller() is DownloadController }) {
+                    setSelectedDrawerItem(R.id.nav_drawer_downloads)
+                }
+            }
+            Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> {
+                //If the intent match the "standard" Android search intent
+                // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant)
+
+                //Get the search query provided in extras, and if not null, perform a global search with it.
+                val query = intent.getStringExtra(SearchManager.QUERY)
+                if (query != null && !query.isEmpty()) {
+                    if (router.backstackSize > 1) {
+                        router.popToRoot()
+                    }
+                    router.pushController(CatalogueSearchController(query).withFadeTransaction())
+                }
+            }
+            INTENT_SEARCH -> {
+                val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
+                val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
+                if (query != null && !query.isEmpty()) {
+                    if (router.backstackSize > 1) {
+                        router.popToRoot()
+                    }
+                    router.pushController(CatalogueSearchController(query, filter).withFadeTransaction())
+                }
+            }
+            else -> return false
+        }
+        return true
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        nav_view?.setNavigationItemSelectedListener(null)
+        toolbar?.setNavigationOnClickListener(null)
+    }
+
+    override fun onBackPressed() {
+        val backstackSize = router.backstackSize
+        if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
+            drawer.closeDrawers()
+        } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
+            setSelectedDrawerItem(startScreenId)
+        } else if (backstackSize == 1 || !router.handleBack()) {
+            super.onBackPressed()
+        }
+    }
+
+    private fun setSelectedDrawerItem(itemId: Int) {
+        if (!isFinishing) {
+            nav_view.setCheckedItem(itemId)
+            nav_view.menu.performIdentifierAction(itemId, 0)
+        }
+    }
+
+    private fun setRoot(controller: Controller, id: Int) {
+        router.setRoot(controller.withFadeTransaction().tag(id.toString()))
+    }
+
+    private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {
+        if (from is DialogController || to is DialogController) {
+            return
+        }
+
+        val showHamburger = router.backstackSize == 1
+        if (showHamburger) {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
+        } else {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+        }
+
+        ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start()
+
+        if (from is TabbedController) {
+            from.cleanupTabs(tabs)
+        }
+        if (to is TabbedController) {
+            tabAnimator.expand()
+            to.configureTabs(tabs)
+        } else {
+            tabAnimator.collapse()
+            tabs.setupWithViewPager(null)
+        }
+
+        if (from is SecondaryDrawerController) {
+            if (secondaryDrawer != null) {
+                from.cleanupSecondaryDrawer(drawer)
+                drawer.removeView(secondaryDrawer)
+                secondaryDrawer = null
+            }
+        }
+        if (to is SecondaryDrawerController) {
+            secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) }
+        }
+
+        if (to is NoToolbarElevationController) {
+            appbar.disableElevation()
+        } else {
+            appbar.enableElevation()
+        }
+    }
+
+    companion object {
+        // Shortcut actions
+        const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
+        const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
+        const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
+        const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
+        const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
+        const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
+
+        const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
+        const val INTENT_SEARCH_QUERY = "query"
+        const val INTENT_SEARCH_FILTER = "filter"
+
+        private const val URL_HELP = "https://tachiyomi.org/help/"
+    }
+
+}

+ 193 - 193
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -1,193 +1,193 @@
-package eu.kanade.tachiyomi.ui.manga
-
-import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
-import android.os.Bundle
-import android.support.design.widget.TabLayout
-import android.support.graphics.drawable.VectorDrawableCompat
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.TextView
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import com.bluelinelabs.conductor.Router
-import com.bluelinelabs.conductor.RouterTransaction
-import com.bluelinelabs.conductor.support.RouterPagerAdapter
-import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.ui.base.controller.RxController
-import eu.kanade.tachiyomi.ui.base.controller.TabbedController
-import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
-import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
-import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
-import eu.kanade.tachiyomi.ui.manga.track.TrackController
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.main_activity.*
-import kotlinx.android.synthetic.main.manga_controller.*
-import rx.Subscription
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.util.Date
-
-class MangaController : RxController, TabbedController {
-
-    constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply {
-        putLong(MANGA_EXTRA, manga?.id ?: 0)
-        putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
-    }) {
-        this.manga = manga
-        if (manga != null) {
-            source = Injekt.get<SourceManager>().getOrStub(manga.source)
-        }
-    }
-
-    constructor(mangaId: Long) : this(
-            Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
-
-    var manga: Manga? = null
-        private set
-
-    var source: Source? = null
-        private set
-
-    private var adapter: MangaDetailAdapter? = null
-
-    val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
-
-    val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
-
-    val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
-
-    val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
-
-    private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
-
-    private var trackingIconSubscription: Subscription? = null
-
-    override fun getTitle(): String? {
-        return manga?.title
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.manga_controller, container, false)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        if (manga == null || source == null) return
-
-        requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
-
-        adapter = MangaDetailAdapter()
-        manga_pager.offscreenPageLimit = 3
-        manga_pager.adapter = adapter
-
-        if (!fromCatalogue)
-            manga_pager.currentItem = CHAPTERS_CONTROLLER
-    }
-
-    override fun onDestroyView(view: View) {
-        super.onDestroyView(view)
-        adapter = null
-    }
-
-    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        super.onChangeStarted(handler, type)
-        if (type.isEnter) {
-            activity?.tabs?.setupWithViewPager(manga_pager)
-            trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
-        }
-    }
-
-    override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        super.onChangeEnded(handler, type)
-        if (manga == null || source == null) {
-            activity?.toast(R.string.manga_not_in_db)
-            router.popController(this)
-        }
-    }
-
-    override fun configureTabs(tabs: TabLayout) {
-        with(tabs) {
-            tabGravity = TabLayout.GRAVITY_FILL
-            tabMode = TabLayout.MODE_FIXED
-        }
-    }
-
-    override fun cleanupTabs(tabs: TabLayout) {
-        trackingIconSubscription?.unsubscribe()
-        setTrackingIconInternal(false)
-    }
-
-    fun setTrackingIcon(visible: Boolean) {
-        trackingIconRelay.call(visible)
-    }
-
-    private fun setTrackingIconInternal(visible: Boolean) {
-        val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
-        val drawable = if (visible)
-            VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
-        else null
-
-        val view = tabField.get(tab) as LinearLayout
-        val textView = view.getChildAt(1) as TextView
-        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
-        textView.compoundDrawablePadding = if (visible) 4 else 0
-    }
-
-    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
-
-        private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
-
-        private val tabTitles = listOf(
-                R.string.manga_detail_tab,
-                R.string.manga_chapters_tab,
-                R.string.manga_tracking_tab)
-                .map { resources!!.getString(it) }
-
-        override fun getCount(): Int {
-            return tabCount
-        }
-
-        override fun configureRouter(router: Router, position: Int) {
-            if (!router.hasRootController()) {
-                val controller = when (position) {
-                    INFO_CONTROLLER -> MangaInfoController()
-                    CHAPTERS_CONTROLLER -> ChaptersController()
-                    TRACK_CONTROLLER -> TrackController()
-                    else -> error("Wrong position $position")
-                }
-                router.setRoot(RouterTransaction.with(controller))
-            }
-        }
-
-        override fun getPageTitle(position: Int): CharSequence {
-            return tabTitles[position]
-        }
-
-    }
-
-    companion object {
-        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
-        const val MANGA_EXTRA = "manga"
-
-        const val INFO_CONTROLLER = 0
-        const val CHAPTERS_CONTROLLER = 1
-        const val TRACK_CONTROLLER = 2
-
-        private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
-                .apply { isAccessible = true }
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga
+
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.os.Bundle
+import android.support.design.widget.TabLayout
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.Router
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.support.RouterPagerAdapter
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.controller.RxController
+import eu.kanade.tachiyomi.ui.base.controller.TabbedController
+import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
+import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
+import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
+import eu.kanade.tachiyomi.ui.manga.track.TrackController
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.main_activity.*
+import kotlinx.android.synthetic.main.manga_controller.*
+import rx.Subscription
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.Date
+
+class MangaController : RxController, TabbedController {
+
+    constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply {
+        putLong(MANGA_EXTRA, manga?.id ?: 0)
+        putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
+    }) {
+        this.manga = manga
+        if (manga != null) {
+            source = Injekt.get<SourceManager>().getOrStub(manga.source)
+        }
+    }
+
+    constructor(mangaId: Long) : this(
+            Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
+
+    var manga: Manga? = null
+        private set
+
+    var source: Source? = null
+        private set
+
+    private var adapter: MangaDetailAdapter? = null
+
+    val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
+
+    val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
+
+    val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
+
+    val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
+
+    private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
+
+    private var trackingIconSubscription: Subscription? = null
+
+    override fun getTitle(): String? {
+        return manga?.title
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.manga_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        if (manga == null || source == null) return
+
+        requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
+
+        adapter = MangaDetailAdapter()
+        manga_pager.offscreenPageLimit = 3
+        manga_pager.adapter = adapter
+
+        if (!fromCatalogue)
+            manga_pager.currentItem = CHAPTERS_CONTROLLER
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+    }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeStarted(handler, type)
+        if (type.isEnter) {
+            activity?.tabs?.setupWithViewPager(manga_pager)
+            trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
+        }
+    }
+
+    override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeEnded(handler, type)
+        if (manga == null || source == null) {
+            activity?.toast(R.string.manga_not_in_db)
+            router.popController(this)
+        }
+    }
+
+    override fun configureTabs(tabs: TabLayout) {
+        with(tabs) {
+            tabGravity = TabLayout.GRAVITY_FILL
+            tabMode = TabLayout.MODE_FIXED
+        }
+    }
+
+    override fun cleanupTabs(tabs: TabLayout) {
+        trackingIconSubscription?.unsubscribe()
+        setTrackingIconInternal(false)
+    }
+
+    fun setTrackingIcon(visible: Boolean) {
+        trackingIconRelay.call(visible)
+    }
+
+    private fun setTrackingIconInternal(visible: Boolean) {
+        val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
+        val drawable = if (visible)
+            VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
+        else null
+
+        val view = tabField.get(tab) as LinearLayout
+        val textView = view.getChildAt(1) as TextView
+        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
+        textView.compoundDrawablePadding = if (visible) 4 else 0
+    }
+
+    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
+
+        private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
+
+        private val tabTitles = listOf(
+                R.string.manga_detail_tab,
+                R.string.manga_chapters_tab,
+                R.string.manga_tracking_tab)
+                .map { resources!!.getString(it) }
+
+        override fun getCount(): Int {
+            return tabCount
+        }
+
+        override fun configureRouter(router: Router, position: Int) {
+            if (!router.hasRootController()) {
+                val controller = when (position) {
+                    INFO_CONTROLLER -> MangaInfoController()
+                    CHAPTERS_CONTROLLER -> ChaptersController()
+                    TRACK_CONTROLLER -> TrackController()
+                    else -> error("Wrong position $position")
+                }
+                router.setRoot(RouterTransaction.with(controller))
+            }
+        }
+
+        override fun getPageTitle(position: Int): CharSequence {
+            return tabTitles[position]
+        }
+
+    }
+
+    companion object {
+        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
+        const val MANGA_EXTRA = "manga"
+
+        const val INFO_CONTROLLER = 0
+        const val CHAPTERS_CONTROLLER = 1
+        const val TRACK_CONTROLLER = 2
+
+        private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
+                .apply { isAccessible = true }
+    }
+
+}

+ 122 - 122
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt

@@ -1,122 +1,122 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.view.View
-import android.widget.PopupMenu
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
-import eu.kanade.tachiyomi.util.getResourceColor
-import eu.kanade.tachiyomi.util.gone
-import eu.kanade.tachiyomi.util.setVectorCompat
-import kotlinx.android.synthetic.main.chapters_item.*
-import java.util.*
-
-class ChapterHolder(
-        private val view: View,
-        private val adapter: ChaptersAdapter
-) : BaseFlexibleViewHolder(view, adapter) {
-
-    init {
-        // We need to post a Runnable to show the popup to make sure that the PopupMenu is
-        // correctly positioned. The reason being that the view may change position before the
-        // PopupMenu is shown.
-        chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
-    }
-
-    fun bind(item: ChapterItem, manga: Manga) {
-        val chapter = item.chapter
-
-        chapter_title.text = when (manga.displayMode) {
-            Manga.DISPLAY_NUMBER -> {
-                val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
-                itemView.context.getString(R.string.display_mode_chapter, number)
-            }
-            else -> chapter.name
-        }
-
-        // Set the correct drawable for dropdown and update the tint to match theme.
-        chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
-
-        // Set correct text color
-        chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
-        if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
-
-        if (chapter.date_upload > 0) {
-            chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
-            chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
-        } else {
-            chapter_date.text = ""
-        }
-
-        //add scanlator if exists
-        chapter_scanlator.text = chapter.scanlator
-        //allow longer titles if there is no scanlator (most sources)
-        if (chapter_scanlator.text.isNullOrBlank()) {
-            chapter_title.maxLines = 2
-            chapter_scanlator.gone()
-        } else {
-            chapter_title.maxLines = 1
-        }
-
-        chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
-            itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
-        } else {
-            ""
-        }
-
-        notifyStatus(item.status)
-    }
-
-    fun notifyStatus(status: Int) = with(download_text) {
-        when (status) {
-            Download.QUEUE -> setText(R.string.chapter_queued)
-            Download.DOWNLOADING -> setText(R.string.chapter_downloading)
-            Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
-            Download.ERROR -> setText(R.string.chapter_error)
-            else -> text = ""
-        }
-    }
-
-    private fun showPopupMenu(view: View) {
-        val item = adapter.getItem(adapterPosition) ?: return
-
-        // Create a PopupMenu, giving it the clicked view for an anchor
-        val popup = PopupMenu(view.context, view)
-
-        // Inflate our menu resource into the PopupMenu's Menu
-        popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
-
-        val chapter = item.chapter
-
-        // Hide download and show delete if the chapter is downloaded
-        if (item.isDownloaded) {
-            popup.menu.findItem(R.id.action_download).isVisible = false
-            popup.menu.findItem(R.id.action_delete).isVisible = true
-        }
-
-        // Hide bookmark if bookmark
-        popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark
-        popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark
-
-        // Hide mark as unread when the chapter is unread
-        if (!chapter.read && chapter.last_page_read == 0) {
-            popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
-        }
-
-        // Hide mark as read when the chapter is read
-        if (chapter.read) {
-            popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
-        }
-
-        // Set a listener so we are notified if a menu item is clicked
-        popup.setOnMenuItemClickListener { menuItem ->
-            adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
-            true
-        }
-
-        // Finally show the PopupMenu
-        popup.show()
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.view.View
+import android.widget.PopupMenu
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.setVectorCompat
+import kotlinx.android.synthetic.main.chapters_item.*
+import java.util.*
+
+class ChapterHolder(
+        private val view: View,
+        private val adapter: ChaptersAdapter
+) : BaseFlexibleViewHolder(view, adapter) {
+
+    init {
+        // We need to post a Runnable to show the popup to make sure that the PopupMenu is
+        // correctly positioned. The reason being that the view may change position before the
+        // PopupMenu is shown.
+        chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
+    }
+
+    fun bind(item: ChapterItem, manga: Manga) {
+        val chapter = item.chapter
+
+        chapter_title.text = when (manga.displayMode) {
+            Manga.DISPLAY_NUMBER -> {
+                val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
+                itemView.context.getString(R.string.display_mode_chapter, number)
+            }
+            else -> chapter.name
+        }
+
+        // Set the correct drawable for dropdown and update the tint to match theme.
+        chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
+
+        // Set correct text color
+        chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
+        if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
+
+        if (chapter.date_upload > 0) {
+            chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
+            chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
+        } else {
+            chapter_date.text = ""
+        }
+
+        //add scanlator if exists
+        chapter_scanlator.text = chapter.scanlator
+        //allow longer titles if there is no scanlator (most sources)
+        if (chapter_scanlator.text.isNullOrBlank()) {
+            chapter_title.maxLines = 2
+            chapter_scanlator.gone()
+        } else {
+            chapter_title.maxLines = 1
+        }
+
+        chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
+            itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
+        } else {
+            ""
+        }
+
+        notifyStatus(item.status)
+    }
+
+    fun notifyStatus(status: Int) = with(download_text) {
+        when (status) {
+            Download.QUEUE -> setText(R.string.chapter_queued)
+            Download.DOWNLOADING -> setText(R.string.chapter_downloading)
+            Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
+            Download.ERROR -> setText(R.string.chapter_error)
+            else -> text = ""
+        }
+    }
+
+    private fun showPopupMenu(view: View) {
+        val item = adapter.getItem(adapterPosition) ?: return
+
+        // Create a PopupMenu, giving it the clicked view for an anchor
+        val popup = PopupMenu(view.context, view)
+
+        // Inflate our menu resource into the PopupMenu's Menu
+        popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
+
+        val chapter = item.chapter
+
+        // Hide download and show delete if the chapter is downloaded
+        if (item.isDownloaded) {
+            popup.menu.findItem(R.id.action_download).isVisible = false
+            popup.menu.findItem(R.id.action_delete).isVisible = true
+        }
+
+        // Hide bookmark if bookmark
+        popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark
+        popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark
+
+        // Hide mark as unread when the chapter is unread
+        if (!chapter.read && chapter.last_page_read == 0) {
+            popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
+        }
+
+        // Hide mark as read when the chapter is read
+        if (chapter.read) {
+            popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
+        }
+
+        // Set a listener so we are notified if a menu item is clicked
+        popup.setOnMenuItemClickListener { menuItem ->
+            adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
+            true
+        }
+
+        // Finally show the PopupMenu
+        popup.show()
+    }
+
+}

+ 52 - 52
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt

@@ -1,53 +1,53 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.view.View
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.model.Download
-
-class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
-        Chapter by chapter {
-
-    private var _status: Int = 0
-
-    var status: Int
-        get() = download?.status ?: _status
-        set(value) { _status = value }
-
-    @Transient var download: Download? = null
-
-    val isDownloaded: Boolean
-        get() = status == Download.DOWNLOADED
-
-    override fun getLayoutRes(): Int {
-        return R.layout.chapters_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ChapterHolder {
-        return ChapterHolder(view, adapter as ChaptersAdapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
-                                holder: ChapterHolder,
-                                position: Int,
-                                payloads: List<Any?>?) {
-
-        holder.bind(this, manga)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other is ChapterItem) {
-            return chapter.id!! == other.chapter.id!!
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return chapter.id!!.hashCode()
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.model.Download
+
+class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
+        Chapter by chapter {
+
+    private var _status: Int = 0
+
+    var status: Int
+        get() = download?.status ?: _status
+        set(value) { _status = value }
+
+    @Transient var download: Download? = null
+
+    val isDownloaded: Boolean
+        get() = status == Download.DOWNLOADED
+
+    override fun getLayoutRes(): Int {
+        return R.layout.chapters_item
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ChapterHolder {
+        return ChapterHolder(view, adapter as ChaptersAdapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: ChapterHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
+        holder.bind(this, manga)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is ChapterItem) {
+            return chapter.id!! == other.chapter.id!!
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return chapter.id!!.hashCode()
+    }
+
 }

+ 45 - 45
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt

@@ -1,45 +1,45 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.content.Context
-import android.view.MenuItem
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.getResourceColor
-import java.text.DateFormat
-import java.text.DecimalFormat
-import java.text.DecimalFormatSymbols
-
-class ChaptersAdapter(
-        controller: ChaptersController,
-        context: Context
-) : FlexibleAdapter<ChapterItem>(null, controller, true) {
-
-    var items: List<ChapterItem> = emptyList()
-
-    val menuItemListener: OnMenuItemClickListener = controller
-
-    val readColor = context.getResourceColor(android.R.attr.textColorHint)
-
-    val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
-
-    val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
-
-    val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
-            .apply { decimalSeparator = '.' })
-
-    val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
-
-    override fun updateDataSet(items: List<ChapterItem>?) {
-        this.items = items ?: emptyList()
-        super.updateDataSet(items)
-    }
-
-    fun indexOf(item: ChapterItem): Int {
-        return items.indexOf(item)
-    }
-
-    interface OnMenuItemClickListener {
-        fun onMenuItemClick(position: Int, item: MenuItem)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.content.Context
+import android.view.MenuItem
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.getResourceColor
+import java.text.DateFormat
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+
+class ChaptersAdapter(
+        controller: ChaptersController,
+        context: Context
+) : FlexibleAdapter<ChapterItem>(null, controller, true) {
+
+    var items: List<ChapterItem> = emptyList()
+
+    val menuItemListener: OnMenuItemClickListener = controller
+
+    val readColor = context.getResourceColor(android.R.attr.textColorHint)
+
+    val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
+
+    val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
+
+    val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
+            .apply { decimalSeparator = '.' })
+
+    val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
+
+    override fun updateDataSet(items: List<ChapterItem>?) {
+        this.items = items ?: emptyList()
+        super.updateDataSet(items)
+    }
+
+    fun indexOf(item: ChapterItem): Int {
+        return items.indexOf(item)
+    }
+
+    interface OnMenuItemClickListener {
+        fun onMenuItemClick(position: Int, item: MenuItem)
+    }
+
+}

+ 486 - 486
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt

@@ -1,486 +1,486 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.content.Intent
-import android.support.design.widget.Snackbar
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.DividerItemDecoration
-import android.support.v7.widget.LinearLayoutManager
-import android.view.*
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.view.clicks
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.SelectableAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.getCoordinates
-import eu.kanade.tachiyomi.util.snack
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.chapters_controller.*
-import timber.log.Timber
-
-class ChaptersController : NucleusController<ChaptersPresenter>(),
-        ActionMode.Callback,
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener,
-        ChaptersAdapter.OnMenuItemClickListener,
-        SetDisplayModeDialog.Listener,
-        SetSortingDialog.Listener,
-        DownloadChaptersDialog.Listener,
-        DownloadCustomChaptersDialog.Listener,
-        DeleteChaptersDialog.Listener {
-
-    /**
-     * Adapter containing a list of chapters.
-     */
-    private var adapter: ChaptersAdapter? = null
-
-    /**
-     * Action mode for multiple selection.
-     */
-    private var actionMode: ActionMode? = null
-
-    /**
-     * Selected items. Used to restore selections after a rotation.
-     */
-    private val selectedItems = mutableSetOf<ChapterItem>()
-
-    init {
-        setHasOptionsMenu(true)
-        setOptionsMenuHidden(true)
-    }
-
-    override fun createPresenter(): ChaptersPresenter {
-        val ctrl = parentController as MangaController
-        return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
-                ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.chapters_controller, container, false)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        // Init RecyclerView and adapter
-        adapter = ChaptersAdapter(this, view.context)
-
-        recycler.adapter = adapter
-        recycler.layoutManager = LinearLayoutManager(view.context)
-        recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
-        recycler.setHasFixedSize(true)
-        adapter?.fastScroller = fast_scroller
-
-        swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
-
-        fab.clicks().subscribeUntilDestroy {
-            val item = presenter.getNextUnreadChapter()
-            if (item != null) {
-                // Create animation listener
-                val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
-                    override fun onAnimationStart(animation: Animator?) {
-                        openChapter(item.chapter, true)
-                    }
-                }
-
-                // Get coordinates and start animation
-                val coordinates = fab.getCoordinates()
-                if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
-                    openChapter(item.chapter)
-                }
-            } else {
-                view.context.toast(R.string.no_next_chapter)
-            }
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        adapter = null
-        actionMode = null
-        super.onDestroyView(view)
-    }
-
-    override fun onActivityResumed(activity: Activity) {
-        if (view == null) return
-
-        // Check if animation view is visible
-        if (reveal_view.visibility == View.VISIBLE) {
-            // Show the unReveal effect
-            val coordinates = fab.getCoordinates()
-            reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
-        }
-        super.onActivityResumed(activity)
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.chapters, menu)
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        // Initialize menu items.
-        val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
-        val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
-        val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
-        val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
-
-        // Set correct checkbox values.
-        menuFilterRead.isChecked = presenter.onlyRead()
-        menuFilterUnread.isChecked = presenter.onlyUnread()
-        menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
-        menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
-
-        if (presenter.onlyRead())
-            //Disable unread filter option if read filter is enabled.
-            menuFilterUnread.isEnabled = false
-        if (presenter.onlyUnread())
-            //Disable read filter option if unread filter is enabled.
-            menuFilterRead.isEnabled = false
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_display_mode -> showDisplayModeDialog()
-            R.id.manga_download -> showDownloadDialog()
-            R.id.action_sorting_mode -> showSortingDialog()
-            R.id.action_filter_unread -> {
-                item.isChecked = !item.isChecked
-                presenter.setUnreadFilter(item.isChecked)
-                activity?.invalidateOptionsMenu()
-            }
-            R.id.action_filter_read -> {
-                item.isChecked = !item.isChecked
-                presenter.setReadFilter(item.isChecked)
-                activity?.invalidateOptionsMenu()
-            }
-            R.id.action_filter_downloaded -> {
-                item.isChecked = !item.isChecked
-                presenter.setDownloadedFilter(item.isChecked)
-            }
-            R.id.action_filter_bookmarked -> {
-                item.isChecked = !item.isChecked
-                presenter.setBookmarkedFilter(item.isChecked)
-            }
-            R.id.action_filter_empty -> {
-                presenter.removeFilters()
-                activity?.invalidateOptionsMenu()
-            }
-            R.id.action_sort -> presenter.revertSortOrder()
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    fun onNextChapters(chapters: List<ChapterItem>) {
-        // If the list is empty, fetch chapters from source if the conditions are met
-        // We use presenter chapters instead because they are always unfiltered
-        if (presenter.chapters.isEmpty())
-            initialFetchChapters()
-
-        val adapter = adapter ?: return
-        adapter.updateDataSet(chapters)
-
-        if (selectedItems.isNotEmpty()) {
-            adapter.clearSelection() // we need to start from a clean state, index may have changed
-            createActionModeIfNeeded()
-            selectedItems.forEach { item ->
-                val position = adapter.indexOf(item)
-                if (position != -1 && !adapter.isSelected(position)) {
-                    adapter.toggleSelection(position)
-                }
-            }
-            actionMode?.invalidate()
-        }
-
-    }
-
-    private fun initialFetchChapters() {
-        // Only fetch if this view is from the catalog and it hasn't requested previously
-        if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
-            fetchChaptersFromSource()
-        }
-    }
-
-    private fun fetchChaptersFromSource() {
-        swipe_refresh?.isRefreshing = true
-        presenter.fetchChaptersFromSource()
-    }
-
-    fun onFetchChaptersDone() {
-        swipe_refresh?.isRefreshing = false
-    }
-
-    fun onFetchChaptersError(error: Throwable) {
-        swipe_refresh?.isRefreshing = false
-        activity?.toast(error.message)
-    }
-
-    fun onChapterStatusChange(download: Download) {
-        getHolder(download.chapter)?.notifyStatus(download.status)
-    }
-
-    private fun getHolder(chapter: Chapter): ChapterHolder? {
-        return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
-    }
-
-    fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
-        val activity = activity ?: return
-        val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
-        if (hasAnimation) {
-            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
-        }
-        startActivity(intent)
-    }
-
-    override fun onItemClick(position: Int): Boolean {
-        val adapter = adapter ?: return false
-        val item = adapter.getItem(position) ?: return false
-        if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openChapter(item.chapter)
-            return false
-        }
-    }
-
-    override fun onItemLongClick(position: Int) {
-        createActionModeIfNeeded()
-        toggleSelection(position)
-    }
-
-    // SELECTIONS & ACTION MODE
-
-    private fun toggleSelection(position: Int) {
-        val adapter = adapter ?: return
-        val item = adapter.getItem(position) ?: return
-        adapter.toggleSelection(position)
-        if (adapter.isSelected(position)) {
-            selectedItems.add(item)
-        } else {
-            selectedItems.remove(item)
-        }
-        actionMode?.invalidate()
-    }
-
-    private fun getSelectedChapters(): List<ChapterItem> {
-        val adapter = adapter ?: return emptyList()
-        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
-    }
-
-    private fun createActionModeIfNeeded() {
-        if (actionMode == null) {
-            actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
-        }
-    }
-
-    private fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.chapter_selection, menu)
-        adapter?.mode = SelectableAdapter.Mode.MULTI
-        return true
-    }
-
-    @SuppressLint("StringFormatInvalid")
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = adapter?.selectedItemCount ?: 0
-        if (count == 0) {
-            // Destroy action mode if there are no items selected.
-            destroyActionModeIfNeeded()
-        } else {
-            mode.title = resources?.getString(R.string.label_selected, count)
-        }
-        return false
-    }
-
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_select_all -> selectAll()
-            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
-            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
-            R.id.action_download -> downloadChapters(getSelectedChapters())
-            R.id.action_delete -> showDeleteChaptersConfirmationDialog()
-            else -> return false
-        }
-        return true
-    }
-
-    override fun onDestroyActionMode(mode: ActionMode) {
-        adapter?.mode = SelectableAdapter.Mode.SINGLE
-        adapter?.clearSelection()
-        selectedItems.clear()
-        actionMode = null
-    }
-
-    override fun onMenuItemClick(position: Int, item: MenuItem) {
-        val chapter = adapter?.getItem(position) ?: return
-        val chapters = listOf(chapter)
-
-        when (item.itemId) {
-            R.id.action_download -> downloadChapters(chapters)
-            R.id.action_bookmark -> bookmarkChapters(chapters, true)
-            R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
-            R.id.action_delete -> deleteChapters(chapters)
-            R.id.action_mark_as_read -> markAsRead(chapters)
-            R.id.action_mark_as_unread -> markAsUnread(chapters)
-            R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
-        }
-    }
-
-    // SELECTION MODE ACTIONS
-
-    private fun selectAll() {
-        val adapter = adapter ?: return
-        adapter.selectAll()
-        selectedItems.addAll(adapter.items)
-        actionMode?.invalidate()
-    }
-
-    private fun markAsRead(chapters: List<ChapterItem>) {
-        presenter.markChaptersRead(chapters, true)
-        if (presenter.preferences.removeAfterMarkedAsRead()) {
-            deleteChapters(chapters)
-        }
-    }
-
-    private fun markAsUnread(chapters: List<ChapterItem>) {
-        presenter.markChaptersRead(chapters, false)
-    }
-
-    private fun downloadChapters(chapters: List<ChapterItem>) {
-        val view = view
-        destroyActionModeIfNeeded()
-        presenter.downloadChapters(chapters)
-        if (view != null && !presenter.manga.favorite) {
-            recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
-                setAction(R.string.action_add) {
-                    presenter.addToLibrary()
-                }
-            }
-        }
-    }
-
-
-    private fun showDeleteChaptersConfirmationDialog() {
-        DeleteChaptersDialog(this).showDialog(router)
-    }
-
-    override fun deleteChapters() {
-        deleteChapters(getSelectedChapters())
-    }
-
-    private fun markPreviousAsRead(chapter: ChapterItem) {
-        val adapter = adapter ?: return
-        val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
-        val chapterPos = chapters.indexOf(chapter)
-        if (chapterPos != -1) {
-            markAsRead(chapters.take(chapterPos))
-        }
-    }
-
-    private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
-        destroyActionModeIfNeeded()
-        presenter.bookmarkChapters(chapters, bookmarked)
-    }
-
-    fun deleteChapters(chapters: List<ChapterItem>) {
-        destroyActionModeIfNeeded()
-        if (chapters.isEmpty()) return
-
-        DeletingChaptersDialog().showDialog(router)
-        presenter.deleteChapters(chapters)
-    }
-
-    fun onChaptersDeleted() {
-        dismissDeletingDialog()
-        adapter?.notifyDataSetChanged()
-    }
-
-    fun onChaptersDeletedError(error: Throwable) {
-        dismissDeletingDialog()
-        Timber.e(error)
-    }
-
-    private fun dismissDeletingDialog() {
-        router.popControllerWithTag(DeletingChaptersDialog.TAG)
-    }
-
-    // OVERFLOW MENU DIALOGS
-
-    private fun showDisplayModeDialog() {
-        val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
-        SetDisplayModeDialog(this, preselected).showDialog(router)
-    }
-
-    override fun setDisplayMode(id: Int) {
-        presenter.setDisplayMode(id)
-        adapter?.notifyDataSetChanged()
-    }
-
-    private fun showSortingDialog() {
-        val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
-        SetSortingDialog(this, preselected).showDialog(router)
-    }
-
-    override fun setSorting(id: Int) {
-        presenter.setSorting(id)
-    }
-
-    private fun showDownloadDialog() {
-        DownloadChaptersDialog(this).showDialog(router)
-    }
-
-    private fun getUnreadChaptersSorted() = presenter.chapters
-            .filter { !it.read && it.status == Download.NOT_DOWNLOADED }
-            .distinctBy { it.name }
-            .sortedByDescending { it.source_order }
-
-    override fun downloadCustomChapters(amount: Int) {
-        val chaptersToDownload = getUnreadChaptersSorted().take(amount)
-        if (chaptersToDownload.isNotEmpty()) {
-            downloadChapters(chaptersToDownload)
-        }
-    }
-
-    private fun showCustomDownloadDialog() {
-        DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
-    }
-
-
-    override fun downloadChapters(choice: Int) {
-        // i = 0: Download 1
-        // i = 1: Download 5
-        // i = 2: Download 10
-        // i = 3: Download x
-        // i = 4: Download unread
-        // i = 5: Download all
-        val chaptersToDownload = when (choice) {
-            0 -> getUnreadChaptersSorted().take(1)
-            1 -> getUnreadChaptersSorted().take(5)
-            2 -> getUnreadChaptersSorted().take(10)
-            3 -> {
-                showCustomDownloadDialog()
-                return
-            }
-            4 -> presenter.chapters.filter { !it.read }
-            5 -> presenter.chapters
-            else -> emptyList()
-        }
-        if (chaptersToDownload.isNotEmpty()) {
-            downloadChapters(chaptersToDownload)
-        }
-    }
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.support.design.widget.Snackbar
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.DividerItemDecoration
+import android.support.v7.widget.LinearLayoutManager
+import android.view.*
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.view.clicks
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.getCoordinates
+import eu.kanade.tachiyomi.util.snack
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.chapters_controller.*
+import timber.log.Timber
+
+class ChaptersController : NucleusController<ChaptersPresenter>(),
+        ActionMode.Callback,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        ChaptersAdapter.OnMenuItemClickListener,
+        SetDisplayModeDialog.Listener,
+        SetSortingDialog.Listener,
+        DownloadChaptersDialog.Listener,
+        DownloadCustomChaptersDialog.Listener,
+        DeleteChaptersDialog.Listener {
+
+    /**
+     * Adapter containing a list of chapters.
+     */
+    private var adapter: ChaptersAdapter? = null
+
+    /**
+     * Action mode for multiple selection.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Selected items. Used to restore selections after a rotation.
+     */
+    private val selectedItems = mutableSetOf<ChapterItem>()
+
+    init {
+        setHasOptionsMenu(true)
+        setOptionsMenuHidden(true)
+    }
+
+    override fun createPresenter(): ChaptersPresenter {
+        val ctrl = parentController as MangaController
+        return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
+                ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.chapters_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        // Init RecyclerView and adapter
+        adapter = ChaptersAdapter(this, view.context)
+
+        recycler.adapter = adapter
+        recycler.layoutManager = LinearLayoutManager(view.context)
+        recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
+        recycler.setHasFixedSize(true)
+        adapter?.fastScroller = fast_scroller
+
+        swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
+
+        fab.clicks().subscribeUntilDestroy {
+            val item = presenter.getNextUnreadChapter()
+            if (item != null) {
+                // Create animation listener
+                val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator?) {
+                        openChapter(item.chapter, true)
+                    }
+                }
+
+                // Get coordinates and start animation
+                val coordinates = fab.getCoordinates()
+                if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
+                    openChapter(item.chapter)
+                }
+            } else {
+                view.context.toast(R.string.no_next_chapter)
+            }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        adapter = null
+        actionMode = null
+        super.onDestroyView(view)
+    }
+
+    override fun onActivityResumed(activity: Activity) {
+        if (view == null) return
+
+        // Check if animation view is visible
+        if (reveal_view.visibility == View.VISIBLE) {
+            // Show the unReveal effect
+            val coordinates = fab.getCoordinates()
+            reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
+        }
+        super.onActivityResumed(activity)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.chapters, menu)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        // Initialize menu items.
+        val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
+        val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
+        val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
+        val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
+
+        // Set correct checkbox values.
+        menuFilterRead.isChecked = presenter.onlyRead()
+        menuFilterUnread.isChecked = presenter.onlyUnread()
+        menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
+        menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
+
+        if (presenter.onlyRead())
+            //Disable unread filter option if read filter is enabled.
+            menuFilterUnread.isEnabled = false
+        if (presenter.onlyUnread())
+            //Disable read filter option if unread filter is enabled.
+            menuFilterRead.isEnabled = false
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_display_mode -> showDisplayModeDialog()
+            R.id.manga_download -> showDownloadDialog()
+            R.id.action_sorting_mode -> showSortingDialog()
+            R.id.action_filter_unread -> {
+                item.isChecked = !item.isChecked
+                presenter.setUnreadFilter(item.isChecked)
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_filter_read -> {
+                item.isChecked = !item.isChecked
+                presenter.setReadFilter(item.isChecked)
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_filter_downloaded -> {
+                item.isChecked = !item.isChecked
+                presenter.setDownloadedFilter(item.isChecked)
+            }
+            R.id.action_filter_bookmarked -> {
+                item.isChecked = !item.isChecked
+                presenter.setBookmarkedFilter(item.isChecked)
+            }
+            R.id.action_filter_empty -> {
+                presenter.removeFilters()
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_sort -> presenter.revertSortOrder()
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    fun onNextChapters(chapters: List<ChapterItem>) {
+        // If the list is empty, fetch chapters from source if the conditions are met
+        // We use presenter chapters instead because they are always unfiltered
+        if (presenter.chapters.isEmpty())
+            initialFetchChapters()
+
+        val adapter = adapter ?: return
+        adapter.updateDataSet(chapters)
+
+        if (selectedItems.isNotEmpty()) {
+            adapter.clearSelection() // we need to start from a clean state, index may have changed
+            createActionModeIfNeeded()
+            selectedItems.forEach { item ->
+                val position = adapter.indexOf(item)
+                if (position != -1 && !adapter.isSelected(position)) {
+                    adapter.toggleSelection(position)
+                }
+            }
+            actionMode?.invalidate()
+        }
+
+    }
+
+    private fun initialFetchChapters() {
+        // Only fetch if this view is from the catalog and it hasn't requested previously
+        if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
+            fetchChaptersFromSource()
+        }
+    }
+
+    private fun fetchChaptersFromSource() {
+        swipe_refresh?.isRefreshing = true
+        presenter.fetchChaptersFromSource()
+    }
+
+    fun onFetchChaptersDone() {
+        swipe_refresh?.isRefreshing = false
+    }
+
+    fun onFetchChaptersError(error: Throwable) {
+        swipe_refresh?.isRefreshing = false
+        activity?.toast(error.message)
+    }
+
+    fun onChapterStatusChange(download: Download) {
+        getHolder(download.chapter)?.notifyStatus(download.status)
+    }
+
+    private fun getHolder(chapter: Chapter): ChapterHolder? {
+        return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
+    }
+
+    fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
+        val activity = activity ?: return
+        val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
+        if (hasAnimation) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+        }
+        startActivity(intent)
+    }
+
+    override fun onItemClick(position: Int): Boolean {
+        val adapter = adapter ?: return false
+        val item = adapter.getItem(position) ?: return false
+        if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openChapter(item.chapter)
+            return false
+        }
+    }
+
+    override fun onItemLongClick(position: Int) {
+        createActionModeIfNeeded()
+        toggleSelection(position)
+    }
+
+    // SELECTIONS & ACTION MODE
+
+    private fun toggleSelection(position: Int) {
+        val adapter = adapter ?: return
+        val item = adapter.getItem(position) ?: return
+        adapter.toggleSelection(position)
+        if (adapter.isSelected(position)) {
+            selectedItems.add(item)
+        } else {
+            selectedItems.remove(item)
+        }
+        actionMode?.invalidate()
+    }
+
+    private fun getSelectedChapters(): List<ChapterItem> {
+        val adapter = adapter ?: return emptyList()
+        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
+    }
+
+    private fun createActionModeIfNeeded() {
+        if (actionMode == null) {
+            actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
+        }
+    }
+
+    private fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.chapter_selection, menu)
+        adapter?.mode = SelectableAdapter.Mode.MULTI
+        return true
+    }
+
+    @SuppressLint("StringFormatInvalid")
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = adapter?.selectedItemCount ?: 0
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+        }
+        return false
+    }
+
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_select_all -> selectAll()
+            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
+            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
+            R.id.action_download -> downloadChapters(getSelectedChapters())
+            R.id.action_delete -> showDeleteChaptersConfirmationDialog()
+            else -> return false
+        }
+        return true
+    }
+
+    override fun onDestroyActionMode(mode: ActionMode) {
+        adapter?.mode = SelectableAdapter.Mode.SINGLE
+        adapter?.clearSelection()
+        selectedItems.clear()
+        actionMode = null
+    }
+
+    override fun onMenuItemClick(position: Int, item: MenuItem) {
+        val chapter = adapter?.getItem(position) ?: return
+        val chapters = listOf(chapter)
+
+        when (item.itemId) {
+            R.id.action_download -> downloadChapters(chapters)
+            R.id.action_bookmark -> bookmarkChapters(chapters, true)
+            R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
+            R.id.action_delete -> deleteChapters(chapters)
+            R.id.action_mark_as_read -> markAsRead(chapters)
+            R.id.action_mark_as_unread -> markAsUnread(chapters)
+            R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
+        }
+    }
+
+    // SELECTION MODE ACTIONS
+
+    private fun selectAll() {
+        val adapter = adapter ?: return
+        adapter.selectAll()
+        selectedItems.addAll(adapter.items)
+        actionMode?.invalidate()
+    }
+
+    private fun markAsRead(chapters: List<ChapterItem>) {
+        presenter.markChaptersRead(chapters, true)
+        if (presenter.preferences.removeAfterMarkedAsRead()) {
+            deleteChapters(chapters)
+        }
+    }
+
+    private fun markAsUnread(chapters: List<ChapterItem>) {
+        presenter.markChaptersRead(chapters, false)
+    }
+
+    private fun downloadChapters(chapters: List<ChapterItem>) {
+        val view = view
+        destroyActionModeIfNeeded()
+        presenter.downloadChapters(chapters)
+        if (view != null && !presenter.manga.favorite) {
+            recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
+                setAction(R.string.action_add) {
+                    presenter.addToLibrary()
+                }
+            }
+        }
+    }
+
+
+    private fun showDeleteChaptersConfirmationDialog() {
+        DeleteChaptersDialog(this).showDialog(router)
+    }
+
+    override fun deleteChapters() {
+        deleteChapters(getSelectedChapters())
+    }
+
+    private fun markPreviousAsRead(chapter: ChapterItem) {
+        val adapter = adapter ?: return
+        val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
+        val chapterPos = chapters.indexOf(chapter)
+        if (chapterPos != -1) {
+            markAsRead(chapters.take(chapterPos))
+        }
+    }
+
+    private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
+        destroyActionModeIfNeeded()
+        presenter.bookmarkChapters(chapters, bookmarked)
+    }
+
+    fun deleteChapters(chapters: List<ChapterItem>) {
+        destroyActionModeIfNeeded()
+        if (chapters.isEmpty()) return
+
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(chapters)
+    }
+
+    fun onChaptersDeleted() {
+        dismissDeletingDialog()
+        adapter?.notifyDataSetChanged()
+    }
+
+    fun onChaptersDeletedError(error: Throwable) {
+        dismissDeletingDialog()
+        Timber.e(error)
+    }
+
+    private fun dismissDeletingDialog() {
+        router.popControllerWithTag(DeletingChaptersDialog.TAG)
+    }
+
+    // OVERFLOW MENU DIALOGS
+
+    private fun showDisplayModeDialog() {
+        val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
+        SetDisplayModeDialog(this, preselected).showDialog(router)
+    }
+
+    override fun setDisplayMode(id: Int) {
+        presenter.setDisplayMode(id)
+        adapter?.notifyDataSetChanged()
+    }
+
+    private fun showSortingDialog() {
+        val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
+        SetSortingDialog(this, preselected).showDialog(router)
+    }
+
+    override fun setSorting(id: Int) {
+        presenter.setSorting(id)
+    }
+
+    private fun showDownloadDialog() {
+        DownloadChaptersDialog(this).showDialog(router)
+    }
+
+    private fun getUnreadChaptersSorted() = presenter.chapters
+            .filter { !it.read && it.status == Download.NOT_DOWNLOADED }
+            .distinctBy { it.name }
+            .sortedByDescending { it.source_order }
+
+    override fun downloadCustomChapters(amount: Int) {
+        val chaptersToDownload = getUnreadChaptersSorted().take(amount)
+        if (chaptersToDownload.isNotEmpty()) {
+            downloadChapters(chaptersToDownload)
+        }
+    }
+
+    private fun showCustomDownloadDialog() {
+        DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
+    }
+
+
+    override fun downloadChapters(choice: Int) {
+        // i = 0: Download 1
+        // i = 1: Download 5
+        // i = 2: Download 10
+        // i = 3: Download x
+        // i = 4: Download unread
+        // i = 5: Download all
+        val chaptersToDownload = when (choice) {
+            0 -> getUnreadChaptersSorted().take(1)
+            1 -> getUnreadChaptersSorted().take(5)
+            2 -> getUnreadChaptersSorted().take(10)
+            3 -> {
+                showCustomDownloadDialog()
+                return
+            }
+            4 -> presenter.chapters.filter { !it.read }
+            5 -> presenter.chapters
+            else -> emptyList()
+        }
+        if (chaptersToDownload.isNotEmpty()) {
+            downloadChapters(chaptersToDownload)
+        }
+    }
+}

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

@@ -1,418 +1,418 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.os.Bundle
-import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import eu.kanade.tachiyomi.util.syncChaptersWithSource
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import timber.log.Timber
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.util.Date
-
-/**
- * Presenter of [ChaptersController].
- */
-class ChaptersPresenter(
-        val manga: Manga,
-        val source: Source,
-        private val chapterCountRelay: BehaviorRelay<Float>,
-        private val lastUpdateRelay: BehaviorRelay<Date>,
-        private val mangaFavoriteRelay: PublishRelay<Boolean>,
-        val preferences: PreferencesHelper = Injekt.get(),
-        private val db: DatabaseHelper = Injekt.get(),
-        private val downloadManager: DownloadManager = Injekt.get()
-) : BasePresenter<ChaptersController>() {
-
-    /**
-     * List of chapters of the manga. It's always unfiltered and unsorted.
-     */
-    var chapters: List<ChapterItem> = emptyList()
-        private set
-
-    /**
-     * Subject of list of chapters to allow updating the view without going to DB.
-     */
-    val chaptersRelay: PublishRelay<List<ChapterItem>>
-            by lazy { PublishRelay.create<List<ChapterItem>>() }
-
-    /**
-     * Whether the chapter list has been requested to the source.
-     */
-    var hasRequested = false
-        private set
-
-    /**
-     * Subscription to retrieve the new list of chapters from the source.
-     */
-    private var fetchChaptersSubscription: Subscription? = null
-
-    /**
-     * Subscription to observe download status changes.
-     */
-    private var observeDownloadsSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        // Prepare the relay.
-        chaptersRelay.flatMap { applyChapterFilters(it) }
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(ChaptersController::onNextChapters,
-                        { _, error -> Timber.e(error) })
-
-        // Add the subscription that retrieves the chapters from the database, keeps subscribed to
-        // changes, and sends the list of chapters to the relay.
-        add(db.getChapters(manga).asRxObservable()
-                .map { chapters ->
-                    // Convert every chapter to a model.
-                    chapters.map { it.toModel() }
-                }
-                .doOnNext { chapters ->
-                    // Find downloaded chapters
-                    setDownloadedChapters(chapters)
-
-                    // Store the last emission
-                    this.chapters = chapters
-
-                    // Listen for download status changes
-                    observeDownloads()
-
-                    // Emit the number of chapters to the info tab.
-                    chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
-                            ?: 0f)
-
-                    // Emit the upload date of the most recent chapter
-                    lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
-                            ?: 0))
-
-                }
-                .subscribe { chaptersRelay.call(it) })
-    }
-
-    private fun observeDownloads() {
-        observeDownloadsSubscription?.let { remove(it) }
-        observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
-                .observeOn(AndroidSchedulers.mainThread())
-                .filter { download -> download.manga.id == manga.id }
-                .doOnNext { onDownloadStatusChange(it) }
-                .subscribeLatestCache(ChaptersController::onChapterStatusChange,
-                        { _, error -> Timber.e(error) })
-    }
-
-    /**
-     * Converts a chapter from the database to an extended model, allowing to store new fields.
-     */
-    private fun Chapter.toModel(): ChapterItem {
-        // Create the model object.
-        val model = ChapterItem(this, manga)
-
-        // Find an active download for this chapter.
-        val download = downloadManager.queue.find { it.chapter.id == id }
-
-        if (download != null) {
-            // If there's an active download, assign it.
-            model.download = download
-        }
-        return model
-    }
-
-    /**
-     * Finds and assigns the list of downloaded chapters.
-     *
-     * @param chapters the list of chapter from the database.
-     */
-    private fun setDownloadedChapters(chapters: List<ChapterItem>) {
-        for (chapter in chapters) {
-            if (downloadManager.isChapterDownloaded(chapter, manga)) {
-                chapter.status = Download.DOWNLOADED
-            }
-        }
-    }
-
-    /**
-     * Requests an updated list of chapters from the source.
-     */
-    fun fetchChaptersFromSource() {
-        hasRequested = true
-
-        if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
-        fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
-                .subscribeOn(Schedulers.io())
-                .map { syncChaptersWithSource(db, it, manga, source) }
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, _ ->
-                    view.onFetchChaptersDone()
-                }, ChaptersController::onFetchChaptersError)
-    }
-
-    /**
-     * Updates the UI after applying the filters.
-     */
-    private fun refreshChapters() {
-        chaptersRelay.call(chapters)
-    }
-
-    /**
-     * Applies the view filters to the list of chapters obtained from the database.
-     * @param chapters the list of chapters from the database
-     * @return an observable of the list of chapters filtered and sorted.
-     */
-    private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
-        var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
-        if (onlyUnread()) {
-            observable = observable.filter { !it.read }
-        }
-        else if (onlyRead()) {
-            observable = observable.filter { it.read }
-        }
-        if (onlyDownloaded()) {
-            observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
-        }
-        if (onlyBookmarked()) {
-            observable = observable.filter { it.bookmark }
-        }
-        val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
-            Manga.SORTING_SOURCE -> when (sortDescending()) {
-                true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
-                false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
-            }
-            Manga.SORTING_NUMBER -> when (sortDescending()) {
-                true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
-                false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
-            }
-            else -> throw NotImplementedError("Unimplemented sorting method")
-        }
-        return observable.toSortedList(sortFunction)
-    }
-
-    /**
-     * Called when a download for the active manga changes status.
-     * @param download the download whose status changed.
-     */
-    fun onDownloadStatusChange(download: Download) {
-        // Assign the download to the model object.
-        if (download.status == Download.QUEUE) {
-            chapters.find { it.id == download.chapter.id }?.let {
-                if (it.download == null) {
-                    it.download = download
-                }
-            }
-        }
-
-        // Force UI update if downloaded filter active and download finished.
-        if (onlyDownloaded() && download.status == Download.DOWNLOADED)
-            refreshChapters()
-    }
-
-    /**
-     * Returns the next unread chapter or null if everything is read.
-     */
-    fun getNextUnreadChapter(): ChapterItem? {
-        return chapters.sortedByDescending { it.source_order }.find { !it.read }
-    }
-
-    /**
-     * Mark the selected chapter list as read/unread.
-     * @param selectedChapters the list of selected chapters.
-     * @param read whether to mark chapters as read or unread.
-     */
-    fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
-        Observable.from(selectedChapters)
-                .doOnNext { chapter ->
-                    chapter.read = read
-                    if (!read) {
-                        chapter.last_page_read = 0
-                    }
-                }
-                .toList()
-                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Downloads the given list of chapters with the manager.
-     * @param chapters the list of chapters to download.
-     */
-    fun downloadChapters(chapters: List<ChapterItem>) {
-        downloadManager.downloadChapters(manga, chapters)
-    }
-
-    /**
-     * Bookmarks the given list of chapters.
-     * @param selectedChapters the list of chapters to bookmark.
-     */
-    fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
-        Observable.from(selectedChapters)
-                .doOnNext { chapter ->
-                    chapter.bookmark = bookmarked
-                }
-                .toList()
-                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Deletes the given list of chapter.
-     * @param chapters the list of chapters to delete.
-     */
-    fun deleteChapters(chapters: List<ChapterItem>) {
-        Observable.just(chapters)
-                .doOnNext { deleteChaptersInternal(chapters) }
-                .doOnNext { if (onlyDownloaded()) refreshChapters() }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, _ ->
-                    view.onChaptersDeleted()
-                }, ChaptersController::onChaptersDeletedError)
-    }
-
-    /**
-     * Deletes a list of chapters from disk. This method is called in a background thread.
-     * @param chapters the chapters to delete.
-     */
-    private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
-        downloadManager.deleteChapters(chapters, manga, source)
-        chapters.forEach {
-            it.status = Download.NOT_DOWNLOADED
-            it.download = null
-        }
-    }
-
-    /**
-     * Reverses the sorting and requests an UI update.
-     */
-    fun revertSortOrder() {
-        manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the read filter and requests an UI update.
-     * @param onlyUnread whether to display only unread chapters or all chapters.
-     */
-    fun setUnreadFilter(onlyUnread: Boolean) {
-        manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the read filter and requests an UI update.
-     * @param onlyRead whether to display only read chapters or all chapters.
-     */
-    fun setReadFilter(onlyRead: Boolean) {
-        manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the download filter and requests an UI update.
-     * @param onlyDownloaded whether to display only downloaded chapters or all chapters.
-     */
-    fun setDownloadedFilter(onlyDownloaded: Boolean) {
-        manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the bookmark filter and requests an UI update.
-     * @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
-     */
-    fun setBookmarkedFilter(onlyBookmarked: Boolean) {
-        manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Removes all filters and requests an UI update.
-     */
-    fun removeFilters() {
-        manga.readFilter = Manga.SHOW_ALL
-        manga.downloadedFilter = Manga.SHOW_ALL
-        manga.bookmarkedFilter = Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Adds manga to library
-     */
-    fun addToLibrary() {
-        mangaFavoriteRelay.call(true)
-    }
-
-    /**
-     * Sets the active display mode.
-     * @param mode the mode to set.
-     */
-    fun setDisplayMode(mode: Int) {
-        manga.displayMode = mode
-        db.updateFlags(manga).executeAsBlocking()
-    }
-
-    /**
-     * Sets the sorting method and requests an UI update.
-     * @param sort the sorting mode.
-     */
-    fun setSorting(sort: Int) {
-        manga.sorting = sort
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Whether the display only downloaded filter is enabled.
-     */
-    fun onlyDownloaded(): Boolean {
-        return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
-    }
-
-    /**
-     * Whether the display only downloaded filter is enabled.
-     */
-    fun onlyBookmarked(): Boolean {
-        return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
-    }
-
-    /**
-     * Whether the display only unread filter is enabled.
-     */
-    fun onlyUnread(): Boolean {
-        return manga.readFilter == Manga.SHOW_UNREAD
-    }
-
-    /**
-     * Whether the display only read filter is enabled.
-     */
-    fun onlyRead(): Boolean {
-        return manga.readFilter == Manga.SHOW_READ
-    }
-
-    /**
-     * Whether the sorting method is descending or ascending.
-     */
-    fun sortDescending(): Boolean {
-        return manga.sortDescending()
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.os.Bundle
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import eu.kanade.tachiyomi.util.syncChaptersWithSource
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.Date
+
+/**
+ * Presenter of [ChaptersController].
+ */
+class ChaptersPresenter(
+        val manga: Manga,
+        val source: Source,
+        private val chapterCountRelay: BehaviorRelay<Float>,
+        private val lastUpdateRelay: BehaviorRelay<Date>,
+        private val mangaFavoriteRelay: PublishRelay<Boolean>,
+        val preferences: PreferencesHelper = Injekt.get(),
+        private val db: DatabaseHelper = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get()
+) : BasePresenter<ChaptersController>() {
+
+    /**
+     * List of chapters of the manga. It's always unfiltered and unsorted.
+     */
+    var chapters: List<ChapterItem> = emptyList()
+        private set
+
+    /**
+     * Subject of list of chapters to allow updating the view without going to DB.
+     */
+    val chaptersRelay: PublishRelay<List<ChapterItem>>
+            by lazy { PublishRelay.create<List<ChapterItem>>() }
+
+    /**
+     * Whether the chapter list has been requested to the source.
+     */
+    var hasRequested = false
+        private set
+
+    /**
+     * Subscription to retrieve the new list of chapters from the source.
+     */
+    private var fetchChaptersSubscription: Subscription? = null
+
+    /**
+     * Subscription to observe download status changes.
+     */
+    private var observeDownloadsSubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+
+        // Prepare the relay.
+        chaptersRelay.flatMap { applyChapterFilters(it) }
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(ChaptersController::onNextChapters,
+                        { _, error -> Timber.e(error) })
+
+        // Add the subscription that retrieves the chapters from the database, keeps subscribed to
+        // changes, and sends the list of chapters to the relay.
+        add(db.getChapters(manga).asRxObservable()
+                .map { chapters ->
+                    // Convert every chapter to a model.
+                    chapters.map { it.toModel() }
+                }
+                .doOnNext { chapters ->
+                    // Find downloaded chapters
+                    setDownloadedChapters(chapters)
+
+                    // Store the last emission
+                    this.chapters = chapters
+
+                    // Listen for download status changes
+                    observeDownloads()
+
+                    // Emit the number of chapters to the info tab.
+                    chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
+                            ?: 0f)
+
+                    // Emit the upload date of the most recent chapter
+                    lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
+                            ?: 0))
+
+                }
+                .subscribe { chaptersRelay.call(it) })
+    }
+
+    private fun observeDownloads() {
+        observeDownloadsSubscription?.let { remove(it) }
+        observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+                .filter { download -> download.manga.id == manga.id }
+                .doOnNext { onDownloadStatusChange(it) }
+                .subscribeLatestCache(ChaptersController::onChapterStatusChange,
+                        { _, error -> Timber.e(error) })
+    }
+
+    /**
+     * Converts a chapter from the database to an extended model, allowing to store new fields.
+     */
+    private fun Chapter.toModel(): ChapterItem {
+        // Create the model object.
+        val model = ChapterItem(this, manga)
+
+        // Find an active download for this chapter.
+        val download = downloadManager.queue.find { it.chapter.id == id }
+
+        if (download != null) {
+            // If there's an active download, assign it.
+            model.download = download
+        }
+        return model
+    }
+
+    /**
+     * Finds and assigns the list of downloaded chapters.
+     *
+     * @param chapters the list of chapter from the database.
+     */
+    private fun setDownloadedChapters(chapters: List<ChapterItem>) {
+        for (chapter in chapters) {
+            if (downloadManager.isChapterDownloaded(chapter, manga)) {
+                chapter.status = Download.DOWNLOADED
+            }
+        }
+    }
+
+    /**
+     * Requests an updated list of chapters from the source.
+     */
+    fun fetchChaptersFromSource() {
+        hasRequested = true
+
+        if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
+        fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
+                .subscribeOn(Schedulers.io())
+                .map { syncChaptersWithSource(db, it, manga, source) }
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, _ ->
+                    view.onFetchChaptersDone()
+                }, ChaptersController::onFetchChaptersError)
+    }
+
+    /**
+     * Updates the UI after applying the filters.
+     */
+    private fun refreshChapters() {
+        chaptersRelay.call(chapters)
+    }
+
+    /**
+     * Applies the view filters to the list of chapters obtained from the database.
+     * @param chapters the list of chapters from the database
+     * @return an observable of the list of chapters filtered and sorted.
+     */
+    private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
+        var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
+        if (onlyUnread()) {
+            observable = observable.filter { !it.read }
+        }
+        else if (onlyRead()) {
+            observable = observable.filter { it.read }
+        }
+        if (onlyDownloaded()) {
+            observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
+        }
+        if (onlyBookmarked()) {
+            observable = observable.filter { it.bookmark }
+        }
+        val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
+            Manga.SORTING_SOURCE -> when (sortDescending()) {
+                true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
+                false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
+            }
+            Manga.SORTING_NUMBER -> when (sortDescending()) {
+                true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
+                false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
+            }
+            else -> throw NotImplementedError("Unimplemented sorting method")
+        }
+        return observable.toSortedList(sortFunction)
+    }
+
+    /**
+     * Called when a download for the active manga changes status.
+     * @param download the download whose status changed.
+     */
+    fun onDownloadStatusChange(download: Download) {
+        // Assign the download to the model object.
+        if (download.status == Download.QUEUE) {
+            chapters.find { it.id == download.chapter.id }?.let {
+                if (it.download == null) {
+                    it.download = download
+                }
+            }
+        }
+
+        // Force UI update if downloaded filter active and download finished.
+        if (onlyDownloaded() && download.status == Download.DOWNLOADED)
+            refreshChapters()
+    }
+
+    /**
+     * Returns the next unread chapter or null if everything is read.
+     */
+    fun getNextUnreadChapter(): ChapterItem? {
+        return chapters.sortedByDescending { it.source_order }.find { !it.read }
+    }
+
+    /**
+     * Mark the selected chapter list as read/unread.
+     * @param selectedChapters the list of selected chapters.
+     * @param read whether to mark chapters as read or unread.
+     */
+    fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
+        Observable.from(selectedChapters)
+                .doOnNext { chapter ->
+                    chapter.read = read
+                    if (!read) {
+                        chapter.last_page_read = 0
+                    }
+                }
+                .toList()
+                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Downloads the given list of chapters with the manager.
+     * @param chapters the list of chapters to download.
+     */
+    fun downloadChapters(chapters: List<ChapterItem>) {
+        downloadManager.downloadChapters(manga, chapters)
+    }
+
+    /**
+     * Bookmarks the given list of chapters.
+     * @param selectedChapters the list of chapters to bookmark.
+     */
+    fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
+        Observable.from(selectedChapters)
+                .doOnNext { chapter ->
+                    chapter.bookmark = bookmarked
+                }
+                .toList()
+                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Deletes the given list of chapter.
+     * @param chapters the list of chapters to delete.
+     */
+    fun deleteChapters(chapters: List<ChapterItem>) {
+        Observable.just(chapters)
+                .doOnNext { deleteChaptersInternal(chapters) }
+                .doOnNext { if (onlyDownloaded()) refreshChapters() }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, _ ->
+                    view.onChaptersDeleted()
+                }, ChaptersController::onChaptersDeletedError)
+    }
+
+    /**
+     * Deletes a list of chapters from disk. This method is called in a background thread.
+     * @param chapters the chapters to delete.
+     */
+    private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
+        downloadManager.deleteChapters(chapters, manga, source)
+        chapters.forEach {
+            it.status = Download.NOT_DOWNLOADED
+            it.download = null
+        }
+    }
+
+    /**
+     * Reverses the sorting and requests an UI update.
+     */
+    fun revertSortOrder() {
+        manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the read filter and requests an UI update.
+     * @param onlyUnread whether to display only unread chapters or all chapters.
+     */
+    fun setUnreadFilter(onlyUnread: Boolean) {
+        manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the read filter and requests an UI update.
+     * @param onlyRead whether to display only read chapters or all chapters.
+     */
+    fun setReadFilter(onlyRead: Boolean) {
+        manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the download filter and requests an UI update.
+     * @param onlyDownloaded whether to display only downloaded chapters or all chapters.
+     */
+    fun setDownloadedFilter(onlyDownloaded: Boolean) {
+        manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the bookmark filter and requests an UI update.
+     * @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
+     */
+    fun setBookmarkedFilter(onlyBookmarked: Boolean) {
+        manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Removes all filters and requests an UI update.
+     */
+    fun removeFilters() {
+        manga.readFilter = Manga.SHOW_ALL
+        manga.downloadedFilter = Manga.SHOW_ALL
+        manga.bookmarkedFilter = Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Adds manga to library
+     */
+    fun addToLibrary() {
+        mangaFavoriteRelay.call(true)
+    }
+
+    /**
+     * Sets the active display mode.
+     * @param mode the mode to set.
+     */
+    fun setDisplayMode(mode: Int) {
+        manga.displayMode = mode
+        db.updateFlags(manga).executeAsBlocking()
+    }
+
+    /**
+     * Sets the sorting method and requests an UI update.
+     * @param sort the sorting mode.
+     */
+    fun setSorting(sort: Int) {
+        manga.sorting = sort
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Whether the display only downloaded filter is enabled.
+     */
+    fun onlyDownloaded(): Boolean {
+        return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
+    }
+
+    /**
+     * Whether the display only downloaded filter is enabled.
+     */
+    fun onlyBookmarked(): Boolean {
+        return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
+    }
+
+    /**
+     * Whether the display only unread filter is enabled.
+     */
+    fun onlyUnread(): Boolean {
+        return manga.readFilter == Manga.SHOW_UNREAD
+    }
+
+    /**
+     * Whether the display only read filter is enabled.
+     */
+    fun onlyRead(): Boolean {
+        return manga.readFilter == Manga.SHOW_READ
+    }
+
+    /**
+     * Whether the sorting method is descending or ascending.
+     */
+    fun sortDescending(): Boolean {
+        return manga.sortDescending()
+    }
+
+}

+ 31 - 31
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt

@@ -1,32 +1,32 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
-        where T : Controller, T : DeleteChaptersDialog.Listener {
-
-    constructor(target: T) : this() {
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        return MaterialDialog.Builder(activity!!)
-                .content(R.string.confirm_delete_chapters)
-                .positiveText(android.R.string.yes)
-                .negativeText(android.R.string.no)
-                .onPositive { _, _ ->
-                    (targetController as? Listener)?.deleteChapters()
-                }
-                .show()
-    }
-
-    interface Listener {
-        fun deleteChapters()
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : DeleteChaptersDialog.Listener {
+
+    constructor(target: T) : this() {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .content(R.string.confirm_delete_chapters)
+                .positiveText(android.R.string.yes)
+                .negativeText(android.R.string.no)
+                .onPositive { _, _ ->
+                    (targetController as? Listener)?.deleteChapters()
+                }
+                .show()
+    }
+
+    interface Listener {
+        fun deleteChapters()
+    }
+
 }

+ 26 - 26
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt

@@ -1,27 +1,27 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Router
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
-
-    companion object {
-        const val TAG = "deleting_dialog"
-    }
-
-    override fun onCreateDialog(savedState: Bundle?): Dialog {
-        return MaterialDialog.Builder(activity!!)
-                .progress(true, 0)
-                .content(R.string.deleting)
-                .build()
-    }
-
-    override fun showDialog(router: Router) {
-        showDialog(router, TAG)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Router
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
+
+    companion object {
+        const val TAG = "deleting_dialog"
+    }
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .progress(true, 0)
+                .content(R.string.deleting)
+                .build()
+    }
+
+    override fun showDialog(router: Router) {
+        showDialog(router, TAG)
+    }
+
 }

+ 41 - 41
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt

@@ -1,42 +1,42 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
-        where T : Controller, T : DownloadChaptersDialog.Listener {
-
-    constructor(target: T) : this() {
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val activity = activity!!
-
-        val choices = intArrayOf(
-                R.string.download_1,
-                R.string.download_5,
-                R.string.download_10,
-                R.string.download_custom,
-                R.string.download_unread,
-                R.string.download_all
-        ).map { activity.getString(it) }
-
-        return MaterialDialog.Builder(activity)
-                .negativeText(android.R.string.cancel)
-                .items(choices)
-                .itemsCallback { _, _, position, _ ->
-                    (targetController as? Listener)?.downloadChapters(position)
-                }
-                .build()
-    }
-
-    interface Listener {
-        fun downloadChapters(choice: Int)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : DownloadChaptersDialog.Listener {
+
+    constructor(target: T) : this() {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+
+        val choices = intArrayOf(
+                R.string.download_1,
+                R.string.download_5,
+                R.string.download_10,
+                R.string.download_custom,
+                R.string.download_unread,
+                R.string.download_all
+        ).map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .negativeText(android.R.string.cancel)
+                .items(choices)
+                .itemsCallback { _, _, position, _ ->
+                    (targetController as? Listener)?.downloadChapters(position)
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun downloadChapters(choice: Int)
+    }
+
 }

+ 42 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt

@@ -1,43 +1,43 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class SetDisplayModeDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
-        where T : Controller, T : SetDisplayModeDialog.Listener {
-
-    private val selectedIndex = args.getInt("selected", -1)
-
-    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
-        putInt("selected", selectedIndex)
-    }) {
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val activity = activity!!
-        val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
-        val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number)
-                .map { activity.getString(it) }
-
-        return MaterialDialog.Builder(activity)
-                .title(R.string.action_display_mode)
-                .items(choices)
-                .itemsIds(ids)
-                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
-                    (targetController as? Listener)?.setDisplayMode(itemView.id)
-                    true
-                }
-                .build()
-    }
-
-    interface Listener {
-        fun setDisplayMode(id: Int)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class SetDisplayModeDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : SetDisplayModeDialog.Listener {
+
+    private val selectedIndex = args.getInt("selected", -1)
+
+    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
+        putInt("selected", selectedIndex)
+    }) {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+        val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
+        val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number)
+                .map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.action_display_mode)
+                .items(choices)
+                .itemsIds(ids)
+                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
+                    (targetController as? Listener)?.setDisplayMode(itemView.id)
+                    true
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun setDisplayMode(id: Int)
+    }
+
 }

+ 42 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt

@@ -1,43 +1,43 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class SetSortingDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
-        where T : Controller, T : SetSortingDialog.Listener {
-
-    private val selectedIndex = args.getInt("selected", -1)
-
-    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
-        putInt("selected", selectedIndex)
-    }) {
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val activity = activity!!
-        val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
-        val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
-                .map { activity.getString(it) }
-
-        return MaterialDialog.Builder(activity)
-                .title(R.string.sorting_mode)
-                .items(choices)
-                .itemsIds(ids)
-                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
-                    (targetController as? Listener)?.setSorting(itemView.id)
-                    true
-                }
-                .build()
-    }
-
-    interface Listener {
-        fun setSorting(id: Int)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class SetSortingDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : SetSortingDialog.Listener {
+
+    private val selectedIndex = args.getInt("selected", -1)
+
+    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
+        putInt("selected", selectedIndex)
+    }) {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+        val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
+        val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
+                .map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.sorting_mode)
+                .items(choices)
+                .itemsIds(ids)
+                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
+                    (targetController as? Listener)?.setSorting(itemView.id)
+                    true
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun setSorting(id: Int)
+    }
+
 }

+ 577 - 577
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt

@@ -1,577 +1,577 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import android.app.Dialog
-import android.app.PendingIntent
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.drawable.Drawable
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.support.customtabs.CustomTabsIntent
-import android.support.v4.content.pm.ShortcutInfoCompat
-import android.support.v4.content.pm.ShortcutManagerCompat
-import android.support.v4.graphics.drawable.IconCompat
-import android.view.*
-import android.widget.Toast
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.target.SimpleTarget
-import com.bumptech.glide.request.transition.Transition
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.view.clicks
-import com.jakewharton.rxbinding.view.longClicks
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.notification.NotificationReceiver
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
-import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
-import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.util.getResourceColor
-import eu.kanade.tachiyomi.util.openInBrowser
-import eu.kanade.tachiyomi.util.snack
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.util.truncateCenter
-import jp.wasabeef.glide.transformations.CropSquareTransformation
-import jp.wasabeef.glide.transformations.MaskTransformation
-import kotlinx.android.synthetic.main.manga_info_controller.*
-import uy.kohesive.injekt.injectLazy
-import java.text.DateFormat
-import java.text.DecimalFormat
-import java.util.Date
-
-/**
- * Fragment that shows manga information.
- * Uses R.layout.manga_info_controller.
- * UI related actions should be called from here.
- */
-class MangaInfoController : NucleusController<MangaInfoPresenter>(),
-        ChangeMangaCategoriesDialog.Listener {
-
-    /**
-     * Preferences helper.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    init {
-        setHasOptionsMenu(true)
-        setOptionsMenuHidden(true)
-    }
-
-    override fun createPresenter(): MangaInfoPresenter {
-        val ctrl = parentController as MangaController
-        return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
-                ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.manga_info_controller, container, false)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        // Set onclickListener to toggle favorite when FAB clicked.
-        fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
-
-        // Set onLongClickListener to manage categories when FAB is clicked.
-        fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
-
-        // Set SwipeRefresh to refresh manga data.
-        swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
-
-        manga_full_title.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString())
-        }
-
-        manga_full_title.clicks().subscribeUntilDestroy {
-            performGlobalSearch(manga_full_title.text.toString())
-        }
-
-        manga_artist.longClicks().subscribeUntilDestroy {
-            copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString())
-        }
-
-        manga_artist.clicks().subscribeUntilDestroy {
-            performGlobalSearch(manga_artist.text.toString())
-        }
-
-        manga_author.longClicks().subscribeUntilDestroy {
-            copyToClipboard(manga_author.text.toString(), manga_author.text.toString())
-        }
-
-        manga_author.clicks().subscribeUntilDestroy {
-            performGlobalSearch(manga_author.text.toString())
-        }
-
-        manga_summary.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString())
-        }
-
-        //manga_genres_tags.setOnTagClickListener { tag -> performGlobalSearch(tag) }
-
-        manga_cover.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
-        }
-
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.manga_info, menu)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_open_in_browser -> openInBrowser()
-            R.id.action_open_in_web_view -> openInWebView()
-            R.id.action_share -> shareManga()
-            R.id.action_add_to_home_screen -> addToHomeScreen()
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    /**
-     * Check if manga is initialized.
-     * If true update view with manga information,
-     * if false fetch manga information
-     *
-     * @param manga  manga object containing information about manga.
-     * @param source the source of the manga.
-     */
-    fun onNextManga(manga: Manga, source: Source) {
-        if (manga.initialized) {
-            // Update view.
-            setMangaInfo(manga, source)
-
-        } else {
-            // Initialize manga.
-            fetchMangaFromSource()
-        }
-    }
-
-    /**
-     * Update the view with manga information.
-     *
-     * @param manga manga object containing information about manga.
-     * @param source the source of the manga.
-     */
-    private fun setMangaInfo(manga: Manga, source: Source?) {
-        val view = view ?: return
-
-        //update full title TextView.
-        manga_full_title.text = if (manga.title.isBlank()) {
-            view.context.getString(R.string.unknown)
-        } else {
-            manga.title
-        }
-
-        // Update artist TextView.
-        manga_artist.text = if (manga.artist.isNullOrBlank()) {
-            view.context.getString(R.string.unknown)
-        } else {
-            manga.artist
-        }
-
-        // Update author TextView.
-        manga_author.text = if (manga.author.isNullOrBlank()) {
-            view.context.getString(R.string.unknown)
-        } else {
-            manga.author
-        }
-
-        // If manga source is known update source TextView.
-        manga_source.text = if (source == null) {
-            view.context.getString(R.string.unknown)
-        } else {
-            source.toString()
-        }
-
-        // Update genres list
-        if (manga.genre.isNullOrBlank().not()) {
-            manga_genres_tags.setTags(manga.genre?.split(", "))
-        }
-
-        // Update description TextView.
-        manga_summary.text = if (manga.description.isNullOrBlank()) {
-            view.context.getString(R.string.unknown)
-        } else {
-            manga.description
-        }
-
-        // Update status TextView.
-        manga_status.setText(when (manga.status) {
-            SManga.ONGOING -> R.string.ongoing
-            SManga.COMPLETED -> R.string.completed
-            SManga.LICENSED -> R.string.licensed
-            else -> R.string.unknown
-        })
-
-        // Set the favorite drawable to the correct one.
-        setFavoriteDrawable(manga.favorite)
-
-        // Set cover if it wasn't already.
-        if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
-            GlideApp.with(view.context)
-                    .load(manga)
-                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                    .centerCrop()
-                    .into(manga_cover)
-
-            if (backdrop != null) {
-                GlideApp.with(view.context)
-                        .load(manga)
-                        .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                        .centerCrop()
-                        .into(backdrop)
-            }
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        manga_genres_tags.setOnTagClickListener(null)
-        super.onDestroyView(view)
-    }
-
-    /**
-     * Update chapter count TextView.
-     *
-     * @param count number of chapters.
-     */
-    fun setChapterCount(count: Float) {
-        if (count > 0f) {
-            manga_chapters?.text = DecimalFormat("#.#").format(count)
-        } else {
-            manga_chapters?.text = resources?.getString(R.string.unknown)
-        }
-    }
-
-    fun setLastUpdateDate(date: Date) {
-        if (date.time != 0L) {
-            manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
-        } else {
-            manga_last_update?.text = resources?.getString(R.string.unknown)
-        }
-    }
-
-    /**
-     * Toggles the favorite status and asks for confirmation to delete downloaded chapters.
-     */
-    private fun toggleFavorite() {
-        val view = view
-
-        val isNowFavorite = presenter.toggleFavorite()
-        if (view != null && !isNowFavorite && presenter.hasDownloads()) {
-            view.snack(view.context.getString(R.string.delete_downloads_for_manga)) {
-                setAction(R.string.action_delete) {
-                    presenter.deleteDownloads()
-                }
-            }
-        }
-    }
-
-    /**
-     * Open the manga in browser.
-     */
-    private fun openInBrowser() {
-        val context = view?.context ?: return
-        val source = presenter.source as? HttpSource ?: return
-
-        context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
-    }
-
-    private fun openInWebView() {
-        val source = presenter.source as? HttpSource ?: return
-
-        val url = try {
-            source.mangaDetailsRequest(presenter.manga).url().toString()
-        } catch (e: Exception) {
-            return
-        }
-
-        parentController?.router?.pushController(MangaWebViewController(source.id, url)
-            .withFadeTransaction())
-    }
-
-    /**
-     * Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
-     */
-    private fun shareManga() {
-        val context = view?.context ?: return
-
-        val source = presenter.source as? HttpSource ?: return
-        try {
-            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
-            val intent = Intent(Intent.ACTION_SEND).apply {
-                type = "text/plain"
-                putExtra(Intent.EXTRA_TEXT, url)
-            }
-            startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
-        } catch (e: Exception) {
-            context.toast(e.message)
-        }
-    }
-
-    /**
-     * Update FAB with correct drawable.
-     *
-     * @param isFavorite determines if manga is favorite or not.
-     */
-    private fun setFavoriteDrawable(isFavorite: Boolean) {
-        // Set the Favorite drawable to the correct one.
-        // Border drawable if false, filled drawable if true.
-        fab_favorite?.setImageResource(if (isFavorite)
-            R.drawable.ic_bookmark_white_24dp
-        else
-            R.drawable.ic_add_to_library_24dp)
-    }
-
-    /**
-     * Start fetching manga information from source.
-     */
-    private fun fetchMangaFromSource() {
-        setRefreshing(true)
-        // Call presenter and start fetching manga information
-        presenter.fetchMangaFromSource()
-    }
-
-
-    /**
-     * Update swipe refresh to stop showing refresh in progress spinner.
-     */
-    fun onFetchMangaDone() {
-        setRefreshing(false)
-    }
-
-    /**
-     * Update swipe refresh to start showing refresh in progress spinner.
-     */
-    fun onFetchMangaError(error: Throwable) {
-        setRefreshing(false)
-        activity?.toast(error.message)
-    }
-
-    /**
-     * Set swipe refresh status.
-     *
-     * @param value whether it should be refreshing or not.
-     */
-    private fun setRefreshing(value: Boolean) {
-        swipe_refresh?.isRefreshing = value
-    }
-
-    /**
-     * Called when the fab is clicked.
-     */
-    private fun onFabClick() {
-        val manga = presenter.manga
-        toggleFavorite()
-        if (manga.favorite) {
-            val categories = presenter.getCategories()
-            val defaultCategoryId = preferences.defaultCategory()
-            val defaultCategory = categories.find { it.id == defaultCategoryId }
-            when {
-                defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
-                defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
-                    presenter.moveMangaToCategory(manga, null)
-                else -> {
-                    val ids = presenter.getMangaCategoryIds(manga)
-                    val preselected = ids.mapNotNull { id ->
-                        categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
-                    }.toTypedArray()
-
-                    ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
-                            .showDialog(router)
-                }
-            }
-            activity?.toast(activity?.getString(R.string.manga_added_library))
-        } else {
-            activity?.toast(activity?.getString(R.string.manga_removed_library))
-        }
-    }
-
-    /**
-     * Called when the fab is long clicked.
-     */
-    private fun onFabLongClick() {
-        val manga = presenter.manga
-        if (!manga.favorite) {
-            toggleFavorite()
-            activity?.toast(activity?.getString(R.string.manga_added_library))
-        }
-        val categories = presenter.getCategories()
-        if (categories.isEmpty()) {
-            // no categories exist, display a message about adding categories
-            activity?.toast(activity?.getString(R.string.action_add_category))
-        } else {
-            val ids = presenter.getMangaCategoryIds(manga)
-            val preselected = ids.mapNotNull { id ->
-                categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
-            }.toTypedArray()
-
-            ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
-                    .showDialog(router)
-        }
-    }
-
-    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
-        val manga = mangas.firstOrNull() ?: return
-        presenter.moveMangaToCategories(manga, categories)
-    }
-
-    /**
-     * Add a shortcut of the manga to the home screen
-     */
-    private fun addToHomeScreen() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            // TODO are transformations really unsupported or is it just the Pixel Launcher?
-            createShortcutForShape()
-        } else {
-            ChooseShapeDialog(this).showDialog(router)
-        }
-    }
-
-    /**
-     * Dialog to choose a shape for the icon.
-     */
-    private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
-
-        constructor(target: MangaInfoController) : this() {
-            targetController = target
-        }
-
-        override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-            val modes = intArrayOf(R.string.circular_icon,
-                    R.string.rounded_icon,
-                    R.string.square_icon,
-                    R.string.star_icon)
-
-            return MaterialDialog.Builder(activity!!)
-                    .title(R.string.icon_shape)
-                    .negativeText(android.R.string.cancel)
-                    .items(modes.map { activity?.getString(it) })
-                    .itemsCallback { _, _, i, _ ->
-                        (targetController as? MangaInfoController)?.createShortcutForShape(i)
-                    }
-                    .build()
-        }
-    }
-
-    /**
-     * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when
-     * the resource is available.
-     *
-     * @param i The shape index to apply. Defaults to circle crop transformation.
-     */
-    private fun createShortcutForShape(i: Int = 0) {
-        if (activity == null) return
-        GlideApp.with(activity!!)
-                .asBitmap()
-                .load(presenter.manga)
-                .diskCacheStrategy(DiskCacheStrategy.NONE)
-                .apply {
-                    when (i) {
-                        0 -> circleCrop()
-                        1 -> transform(RoundedCorners(5))
-                        2 -> transform(CropSquareTransformation())
-                        3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star))
-                    }
-                }
-                .into(object : SimpleTarget<Bitmap>(96, 96) {
-                    override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
-                        createShortcut(resource)
-                    }
-
-                    override fun onLoadFailed(errorDrawable: Drawable?) {
-                        activity?.toast(R.string.icon_creation_fail)
-                    }
-                })
-    }
-
-    /**
-     * Copies a string to clipboard
-     *
-     * @param label Label to show to the user describing the content
-     * @param content the actual text to copy to the board
-     */
-    private fun copyToClipboard(label: String, content: String) {
-        if (content.isBlank()) return
-
-        val activity = activity ?: return
-        val view = view ?: return
-
-        val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
-        clipboard.primaryClip = ClipData.newPlainText(label, content)
-
-        activity.toast(view.context.getString(R.string.copied_to_clipboard, content.truncateCenter(20)),
-                Toast.LENGTH_SHORT)
-    }
-
-    /**
-     * Perform a global search using the provided query.
-     *
-     * @param query the search query to pass to the search controller
-     */
-    fun performGlobalSearch(query: String) {
-        val router = parentController?.router ?: return
-        router.pushController(CatalogueSearchController(query).withFadeTransaction())
-    }
-
-    /**
-     * Create shortcut using ShortcutManager.
-     *
-     * @param icon The image of the shortcut.
-     */
-    private fun createShortcut(icon: Bitmap) {
-        val activity = activity ?: return
-        val mangaControllerArgs = parentController?.args ?: return
-
-        // Create the shortcut intent.
-        val shortcutIntent = activity.intent
-                .setAction(MainActivity.SHORTCUT_MANGA)
-                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-                .putExtra(MangaController.MANGA_EXTRA,
-                        mangaControllerArgs.getLong(MangaController.MANGA_EXTRA))
-
-        // Check if shortcut placement is supported
-        if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
-            val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}"
-
-            // Create shortcut info
-            val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId)
-                    .setShortLabel(presenter.manga.title)
-                    .setIcon(IconCompat.createWithBitmap(icon))
-                    .setIntent(shortcutIntent)
-                    .build()
-
-            val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                // Create the CallbackIntent.
-                val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo)
-
-                // Configure the intent so that the broadcast receiver gets the callback successfully.
-                PendingIntent.getBroadcast(activity, 0, intent, 0)
-            } else {
-                NotificationReceiver.shortcutCreatedBroadcast(activity)
-            }
-
-            // Request shortcut.
-            ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo,
-                    successCallback.intentSender)
-        }
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.info
+
+import android.app.Dialog
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.support.customtabs.CustomTabsIntent
+import android.support.v4.content.pm.ShortcutInfoCompat
+import android.support.v4.content.pm.ShortcutManagerCompat
+import android.support.v4.graphics.drawable.IconCompat
+import android.view.*
+import android.widget.Toast
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.target.SimpleTarget
+import com.bumptech.glide.request.transition.Transition
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.view.clicks
+import com.jakewharton.rxbinding.view.longClicks
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
+import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.openInBrowser
+import eu.kanade.tachiyomi.util.snack
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.util.truncateCenter
+import jp.wasabeef.glide.transformations.CropSquareTransformation
+import jp.wasabeef.glide.transformations.MaskTransformation
+import kotlinx.android.synthetic.main.manga_info_controller.*
+import uy.kohesive.injekt.injectLazy
+import java.text.DateFormat
+import java.text.DecimalFormat
+import java.util.Date
+
+/**
+ * Fragment that shows manga information.
+ * Uses R.layout.manga_info_controller.
+ * UI related actions should be called from here.
+ */
+class MangaInfoController : NucleusController<MangaInfoPresenter>(),
+        ChangeMangaCategoriesDialog.Listener {
+
+    /**
+     * Preferences helper.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    init {
+        setHasOptionsMenu(true)
+        setOptionsMenuHidden(true)
+    }
+
+    override fun createPresenter(): MangaInfoPresenter {
+        val ctrl = parentController as MangaController
+        return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
+                ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.manga_info_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        // Set onclickListener to toggle favorite when FAB clicked.
+        fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
+
+        // Set onLongClickListener to manage categories when FAB is clicked.
+        fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
+
+        // Set SwipeRefresh to refresh manga data.
+        swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
+
+        manga_full_title.longClicks().subscribeUntilDestroy {
+            copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString())
+        }
+
+        manga_full_title.clicks().subscribeUntilDestroy {
+            performGlobalSearch(manga_full_title.text.toString())
+        }
+
+        manga_artist.longClicks().subscribeUntilDestroy {
+            copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString())
+        }
+
+        manga_artist.clicks().subscribeUntilDestroy {
+            performGlobalSearch(manga_artist.text.toString())
+        }
+
+        manga_author.longClicks().subscribeUntilDestroy {
+            copyToClipboard(manga_author.text.toString(), manga_author.text.toString())
+        }
+
+        manga_author.clicks().subscribeUntilDestroy {
+            performGlobalSearch(manga_author.text.toString())
+        }
+
+        manga_summary.longClicks().subscribeUntilDestroy {
+            copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString())
+        }
+
+        //manga_genres_tags.setOnTagClickListener { tag -> performGlobalSearch(tag) }
+
+        manga_cover.longClicks().subscribeUntilDestroy {
+            copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
+        }
+
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.manga_info, menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_open_in_browser -> openInBrowser()
+            R.id.action_open_in_web_view -> openInWebView()
+            R.id.action_share -> shareManga()
+            R.id.action_add_to_home_screen -> addToHomeScreen()
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    /**
+     * Check if manga is initialized.
+     * If true update view with manga information,
+     * if false fetch manga information
+     *
+     * @param manga  manga object containing information about manga.
+     * @param source the source of the manga.
+     */
+    fun onNextManga(manga: Manga, source: Source) {
+        if (manga.initialized) {
+            // Update view.
+            setMangaInfo(manga, source)
+
+        } else {
+            // Initialize manga.
+            fetchMangaFromSource()
+        }
+    }
+
+    /**
+     * Update the view with manga information.
+     *
+     * @param manga manga object containing information about manga.
+     * @param source the source of the manga.
+     */
+    private fun setMangaInfo(manga: Manga, source: Source?) {
+        val view = view ?: return
+
+        //update full title TextView.
+        manga_full_title.text = if (manga.title.isBlank()) {
+            view.context.getString(R.string.unknown)
+        } else {
+            manga.title
+        }
+
+        // Update artist TextView.
+        manga_artist.text = if (manga.artist.isNullOrBlank()) {
+            view.context.getString(R.string.unknown)
+        } else {
+            manga.artist
+        }
+
+        // Update author TextView.
+        manga_author.text = if (manga.author.isNullOrBlank()) {
+            view.context.getString(R.string.unknown)
+        } else {
+            manga.author
+        }
+
+        // If manga source is known update source TextView.
+        manga_source.text = if (source == null) {
+            view.context.getString(R.string.unknown)
+        } else {
+            source.toString()
+        }
+
+        // Update genres list
+        if (manga.genre.isNullOrBlank().not()) {
+            manga_genres_tags.setTags(manga.genre?.split(", "))
+        }
+
+        // Update description TextView.
+        manga_summary.text = if (manga.description.isNullOrBlank()) {
+            view.context.getString(R.string.unknown)
+        } else {
+            manga.description
+        }
+
+        // Update status TextView.
+        manga_status.setText(when (manga.status) {
+            SManga.ONGOING -> R.string.ongoing
+            SManga.COMPLETED -> R.string.completed
+            SManga.LICENSED -> R.string.licensed
+            else -> R.string.unknown
+        })
+
+        // Set the favorite drawable to the correct one.
+        setFavoriteDrawable(manga.favorite)
+
+        // Set cover if it wasn't already.
+        if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
+            GlideApp.with(view.context)
+                    .load(manga)
+                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                    .centerCrop()
+                    .into(manga_cover)
+
+            if (backdrop != null) {
+                GlideApp.with(view.context)
+                        .load(manga)
+                        .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                        .centerCrop()
+                        .into(backdrop)
+            }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        manga_genres_tags.setOnTagClickListener(null)
+        super.onDestroyView(view)
+    }
+
+    /**
+     * Update chapter count TextView.
+     *
+     * @param count number of chapters.
+     */
+    fun setChapterCount(count: Float) {
+        if (count > 0f) {
+            manga_chapters?.text = DecimalFormat("#.#").format(count)
+        } else {
+            manga_chapters?.text = resources?.getString(R.string.unknown)
+        }
+    }
+
+    fun setLastUpdateDate(date: Date) {
+        if (date.time != 0L) {
+            manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
+        } else {
+            manga_last_update?.text = resources?.getString(R.string.unknown)
+        }
+    }
+
+    /**
+     * Toggles the favorite status and asks for confirmation to delete downloaded chapters.
+     */
+    private fun toggleFavorite() {
+        val view = view
+
+        val isNowFavorite = presenter.toggleFavorite()
+        if (view != null && !isNowFavorite && presenter.hasDownloads()) {
+            view.snack(view.context.getString(R.string.delete_downloads_for_manga)) {
+                setAction(R.string.action_delete) {
+                    presenter.deleteDownloads()
+                }
+            }
+        }
+    }
+
+    /**
+     * Open the manga in browser.
+     */
+    private fun openInBrowser() {
+        val context = view?.context ?: return
+        val source = presenter.source as? HttpSource ?: return
+
+        context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
+    }
+
+    private fun openInWebView() {
+        val source = presenter.source as? HttpSource ?: return
+
+        val url = try {
+            source.mangaDetailsRequest(presenter.manga).url().toString()
+        } catch (e: Exception) {
+            return
+        }
+
+        parentController?.router?.pushController(MangaWebViewController(source.id, url)
+            .withFadeTransaction())
+    }
+
+    /**
+     * Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
+     */
+    private fun shareManga() {
+        val context = view?.context ?: return
+
+        val source = presenter.source as? HttpSource ?: return
+        try {
+            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
+            val intent = Intent(Intent.ACTION_SEND).apply {
+                type = "text/plain"
+                putExtra(Intent.EXTRA_TEXT, url)
+            }
+            startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
+        } catch (e: Exception) {
+            context.toast(e.message)
+        }
+    }
+
+    /**
+     * Update FAB with correct drawable.
+     *
+     * @param isFavorite determines if manga is favorite or not.
+     */
+    private fun setFavoriteDrawable(isFavorite: Boolean) {
+        // Set the Favorite drawable to the correct one.
+        // Border drawable if false, filled drawable if true.
+        fab_favorite?.setImageResource(if (isFavorite)
+            R.drawable.ic_bookmark_white_24dp
+        else
+            R.drawable.ic_add_to_library_24dp)
+    }
+
+    /**
+     * Start fetching manga information from source.
+     */
+    private fun fetchMangaFromSource() {
+        setRefreshing(true)
+        // Call presenter and start fetching manga information
+        presenter.fetchMangaFromSource()
+    }
+
+
+    /**
+     * Update swipe refresh to stop showing refresh in progress spinner.
+     */
+    fun onFetchMangaDone() {
+        setRefreshing(false)
+    }
+
+    /**
+     * Update swipe refresh to start showing refresh in progress spinner.
+     */
+    fun onFetchMangaError(error: Throwable) {
+        setRefreshing(false)
+        activity?.toast(error.message)
+    }
+
+    /**
+     * Set swipe refresh status.
+     *
+     * @param value whether it should be refreshing or not.
+     */
+    private fun setRefreshing(value: Boolean) {
+        swipe_refresh?.isRefreshing = value
+    }
+
+    /**
+     * Called when the fab is clicked.
+     */
+    private fun onFabClick() {
+        val manga = presenter.manga
+        toggleFavorite()
+        if (manga.favorite) {
+            val categories = presenter.getCategories()
+            val defaultCategoryId = preferences.defaultCategory()
+            val defaultCategory = categories.find { it.id == defaultCategoryId }
+            when {
+                defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
+                defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
+                    presenter.moveMangaToCategory(manga, null)
+                else -> {
+                    val ids = presenter.getMangaCategoryIds(manga)
+                    val preselected = ids.mapNotNull { id ->
+                        categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
+                    }.toTypedArray()
+
+                    ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
+                            .showDialog(router)
+                }
+            }
+            activity?.toast(activity?.getString(R.string.manga_added_library))
+        } else {
+            activity?.toast(activity?.getString(R.string.manga_removed_library))
+        }
+    }
+
+    /**
+     * Called when the fab is long clicked.
+     */
+    private fun onFabLongClick() {
+        val manga = presenter.manga
+        if (!manga.favorite) {
+            toggleFavorite()
+            activity?.toast(activity?.getString(R.string.manga_added_library))
+        }
+        val categories = presenter.getCategories()
+        if (categories.isEmpty()) {
+            // no categories exist, display a message about adding categories
+            activity?.toast(activity?.getString(R.string.action_add_category))
+        } else {
+            val ids = presenter.getMangaCategoryIds(manga)
+            val preselected = ids.mapNotNull { id ->
+                categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
+            }.toTypedArray()
+
+            ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
+                    .showDialog(router)
+        }
+    }
+
+    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
+        val manga = mangas.firstOrNull() ?: return
+        presenter.moveMangaToCategories(manga, categories)
+    }
+
+    /**
+     * Add a shortcut of the manga to the home screen
+     */
+    private fun addToHomeScreen() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            // TODO are transformations really unsupported or is it just the Pixel Launcher?
+            createShortcutForShape()
+        } else {
+            ChooseShapeDialog(this).showDialog(router)
+        }
+    }
+
+    /**
+     * Dialog to choose a shape for the icon.
+     */
+    private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
+
+        constructor(target: MangaInfoController) : this() {
+            targetController = target
+        }
+
+        override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+            val modes = intArrayOf(R.string.circular_icon,
+                    R.string.rounded_icon,
+                    R.string.square_icon,
+                    R.string.star_icon)
+
+            return MaterialDialog.Builder(activity!!)
+                    .title(R.string.icon_shape)
+                    .negativeText(android.R.string.cancel)
+                    .items(modes.map { activity?.getString(it) })
+                    .itemsCallback { _, _, i, _ ->
+                        (targetController as? MangaInfoController)?.createShortcutForShape(i)
+                    }
+                    .build()
+        }
+    }
+
+    /**
+     * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when
+     * the resource is available.
+     *
+     * @param i The shape index to apply. Defaults to circle crop transformation.
+     */
+    private fun createShortcutForShape(i: Int = 0) {
+        if (activity == null) return
+        GlideApp.with(activity!!)
+                .asBitmap()
+                .load(presenter.manga)
+                .diskCacheStrategy(DiskCacheStrategy.NONE)
+                .apply {
+                    when (i) {
+                        0 -> circleCrop()
+                        1 -> transform(RoundedCorners(5))
+                        2 -> transform(CropSquareTransformation())
+                        3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star))
+                    }
+                }
+                .into(object : SimpleTarget<Bitmap>(96, 96) {
+                    override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
+                        createShortcut(resource)
+                    }
+
+                    override fun onLoadFailed(errorDrawable: Drawable?) {
+                        activity?.toast(R.string.icon_creation_fail)
+                    }
+                })
+    }
+
+    /**
+     * Copies a string to clipboard
+     *
+     * @param label Label to show to the user describing the content
+     * @param content the actual text to copy to the board
+     */
+    private fun copyToClipboard(label: String, content: String) {
+        if (content.isBlank()) return
+
+        val activity = activity ?: return
+        val view = view ?: return
+
+        val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+        clipboard.primaryClip = ClipData.newPlainText(label, content)
+
+        activity.toast(view.context.getString(R.string.copied_to_clipboard, content.truncateCenter(20)),
+                Toast.LENGTH_SHORT)
+    }
+
+    /**
+     * Perform a global search using the provided query.
+     *
+     * @param query the search query to pass to the search controller
+     */
+    fun performGlobalSearch(query: String) {
+        val router = parentController?.router ?: return
+        router.pushController(CatalogueSearchController(query).withFadeTransaction())
+    }
+
+    /**
+     * Create shortcut using ShortcutManager.
+     *
+     * @param icon The image of the shortcut.
+     */
+    private fun createShortcut(icon: Bitmap) {
+        val activity = activity ?: return
+        val mangaControllerArgs = parentController?.args ?: return
+
+        // Create the shortcut intent.
+        val shortcutIntent = activity.intent
+                .setAction(MainActivity.SHORTCUT_MANGA)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                .putExtra(MangaController.MANGA_EXTRA,
+                        mangaControllerArgs.getLong(MangaController.MANGA_EXTRA))
+
+        // Check if shortcut placement is supported
+        if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
+            val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}"
+
+            // Create shortcut info
+            val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId)
+                    .setShortLabel(presenter.manga.title)
+                    .setIcon(IconCompat.createWithBitmap(icon))
+                    .setIntent(shortcutIntent)
+                    .build()
+
+            val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                // Create the CallbackIntent.
+                val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo)
+
+                // Configure the intent so that the broadcast receiver gets the callback successfully.
+                PendingIntent.getBroadcast(activity, 0, intent, 0)
+            } else {
+                NotificationReceiver.shortcutCreatedBroadcast(activity)
+            }
+
+            // Request shortcut.
+            ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo,
+                    successCallback.intentSender)
+        }
+    }
+
+}

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

@@ -1,173 +1,173 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import android.os.Bundle
-import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.util.*
-
-/**
- * Presenter of MangaInfoFragment.
- * Contains information and data for fragment.
- * Observable updates should be called from here.
- */
-class MangaInfoPresenter(
-        val manga: Manga,
-        val source: Source,
-        private val chapterCountRelay: BehaviorRelay<Float>,
-        private val lastUpdateRelay: BehaviorRelay<Date>,
-        private val mangaFavoriteRelay: PublishRelay<Boolean>,
-        private val db: DatabaseHelper = Injekt.get(),
-        private val downloadManager: DownloadManager = Injekt.get(),
-        private val coverCache: CoverCache = Injekt.get()
-) : BasePresenter<MangaInfoController>() {
-
-    /**
-     * Subscription to send the manga to the view.
-     */
-    private var viewMangaSubscription: Subscription? = null
-
-    /**
-     * Subscription to update the manga from the source.
-     */
-    private var fetchMangaSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        sendMangaToView()
-
-        // Update chapter count
-        chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(MangaInfoController::setChapterCount)
-
-        // Update favorite status
-        mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
-                .subscribe { setFavorite(it) }
-                .apply { add(this) }
-
-        //update last update date
-        lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(MangaInfoController::setLastUpdateDate)
-    }
-
-    /**
-     * Sends the active manga to the view.
-     */
-    fun sendMangaToView() {
-        viewMangaSubscription?.let { remove(it) }
-        viewMangaSubscription = Observable.just(manga)
-                .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
-    }
-
-    /**
-     * Fetch manga information from source.
-     */
-    fun fetchMangaFromSource() {
-        if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
-        fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
-                .map { networkManga ->
-                    manga.copyFrom(networkManga)
-                    manga.initialized = true
-                    db.insertManga(manga).executeAsBlocking()
-                    manga
-                }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .doOnNext { sendMangaToView() }
-                .subscribeFirst({ view, _ ->
-                    view.onFetchMangaDone()
-                }, MangaInfoController::onFetchMangaError)
-    }
-
-    /**
-     * Update favorite status of manga, (removes / adds) manga (to / from) library.
-     *
-     * @return the new status of the manga.
-     */
-    fun toggleFavorite(): Boolean {
-        manga.favorite = !manga.favorite
-        if (!manga.favorite) {
-            coverCache.deleteFromCache(manga.thumbnail_url)
-        }
-        db.insertManga(manga).executeAsBlocking()
-        sendMangaToView()
-        return manga.favorite
-    }
-
-    private fun setFavorite(favorite: Boolean) {
-        if (manga.favorite == favorite) {
-            return
-        }
-        toggleFavorite()
-    }
-
-    /**
-     * Returns true if the manga has any downloads.
-     */
-    fun hasDownloads(): Boolean {
-        return downloadManager.getDownloadCount(manga) > 0
-    }
-
-    /**
-     * Deletes all the downloads for the manga.
-     */
-    fun deleteDownloads() {
-        downloadManager.deleteManga(manga, source)
-    }
-
-    /**
-     * Get user categories.
-     *
-     * @return List of categories, not including the default category
-     */
-    fun getCategories(): List<Category> {
-        return db.getCategories().executeAsBlocking()
-    }
-
-    /**
-     * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
-     *
-     * @param manga the manga to get categories from.
-     * @return Array of category ids the manga is in, if none returns default id
-     */
-    fun getMangaCategoryIds(manga: Manga): Array<Int> {
-        val categories = db.getCategoriesForManga(manga).executeAsBlocking()
-        return categories.mapNotNull { it.id }.toTypedArray()
-    }
-
-    /**
-     * Move the given manga to categories.
-     *
-     * @param manga the manga to move.
-     * @param categories the selected categories.
-     */
-    fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
-        val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
-        db.setMangaCategories(mc, listOf(manga))
-    }
-
-    /**
-     * Move the given manga to the category.
-     *
-     * @param manga the manga to move.
-     * @param category the selected category, or null for default category.
-     */
-    fun moveMangaToCategory(manga: Manga, category: Category?) {
-        moveMangaToCategories(manga, listOfNotNull(category))
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.info
+
+import android.os.Bundle
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaCategory
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.*
+
+/**
+ * Presenter of MangaInfoFragment.
+ * Contains information and data for fragment.
+ * Observable updates should be called from here.
+ */
+class MangaInfoPresenter(
+        val manga: Manga,
+        val source: Source,
+        private val chapterCountRelay: BehaviorRelay<Float>,
+        private val lastUpdateRelay: BehaviorRelay<Date>,
+        private val mangaFavoriteRelay: PublishRelay<Boolean>,
+        private val db: DatabaseHelper = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get(),
+        private val coverCache: CoverCache = Injekt.get()
+) : BasePresenter<MangaInfoController>() {
+
+    /**
+     * Subscription to send the manga to the view.
+     */
+    private var viewMangaSubscription: Subscription? = null
+
+    /**
+     * Subscription to update the manga from the source.
+     */
+    private var fetchMangaSubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        sendMangaToView()
+
+        // Update chapter count
+        chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(MangaInfoController::setChapterCount)
+
+        // Update favorite status
+        mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
+                .subscribe { setFavorite(it) }
+                .apply { add(this) }
+
+        //update last update date
+        lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(MangaInfoController::setLastUpdateDate)
+    }
+
+    /**
+     * Sends the active manga to the view.
+     */
+    fun sendMangaToView() {
+        viewMangaSubscription?.let { remove(it) }
+        viewMangaSubscription = Observable.just(manga)
+                .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
+    }
+
+    /**
+     * Fetch manga information from source.
+     */
+    fun fetchMangaFromSource() {
+        if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
+        fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
+                .map { networkManga ->
+                    manga.copyFrom(networkManga)
+                    manga.initialized = true
+                    db.insertManga(manga).executeAsBlocking()
+                    manga
+                }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnNext { sendMangaToView() }
+                .subscribeFirst({ view, _ ->
+                    view.onFetchMangaDone()
+                }, MangaInfoController::onFetchMangaError)
+    }
+
+    /**
+     * Update favorite status of manga, (removes / adds) manga (to / from) library.
+     *
+     * @return the new status of the manga.
+     */
+    fun toggleFavorite(): Boolean {
+        manga.favorite = !manga.favorite
+        if (!manga.favorite) {
+            coverCache.deleteFromCache(manga.thumbnail_url)
+        }
+        db.insertManga(manga).executeAsBlocking()
+        sendMangaToView()
+        return manga.favorite
+    }
+
+    private fun setFavorite(favorite: Boolean) {
+        if (manga.favorite == favorite) {
+            return
+        }
+        toggleFavorite()
+    }
+
+    /**
+     * Returns true if the manga has any downloads.
+     */
+    fun hasDownloads(): Boolean {
+        return downloadManager.getDownloadCount(manga) > 0
+    }
+
+    /**
+     * Deletes all the downloads for the manga.
+     */
+    fun deleteDownloads() {
+        downloadManager.deleteManga(manga, source)
+    }
+
+    /**
+     * Get user categories.
+     *
+     * @return List of categories, not including the default category
+     */
+    fun getCategories(): List<Category> {
+        return db.getCategories().executeAsBlocking()
+    }
+
+    /**
+     * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
+     *
+     * @param manga the manga to get categories from.
+     * @return Array of category ids the manga is in, if none returns default id
+     */
+    fun getMangaCategoryIds(manga: Manga): Array<Int> {
+        val categories = db.getCategoriesForManga(manga).executeAsBlocking()
+        return categories.mapNotNull { it.id }.toTypedArray()
+    }
+
+    /**
+     * Move the given manga to categories.
+     *
+     * @param manga the manga to move.
+     * @param categories the selected categories.
+     */
+    fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
+        val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
+        db.setMangaCategories(mc, listOf(manga))
+    }
+
+    /**
+     * Move the given manga to the category.
+     *
+     * @param manga the manga to move.
+     * @param category the selected category, or null for default category.
+     */
+    fun moveMangaToCategory(manga: Manga, category: Category?) {
+        moveMangaToCategories(manga, listOfNotNull(category))
+    }
+
+}

+ 73 - 73
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt

@@ -1,74 +1,74 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.app.Dialog
-import android.os.Bundle
-import android.widget.NumberPicker
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class SetTrackChaptersDialog<T> : DialogController
-        where T : Controller, T : SetTrackChaptersDialog.Listener {
-
-    private val item: TrackItem
-
-    constructor(target: T, item: TrackItem) : super(Bundle().apply {
-        putSerializable(KEY_ITEM_TRACK, item.track)
-    }) {
-        targetController = target
-        this.item = item
-    }
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : super(bundle) {
-        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
-        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
-        item = TrackItem(track, service)
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val item = item
-
-        val dialog = MaterialDialog.Builder(activity!!)
-                .title(R.string.chapters)
-                .customView(R.layout.track_chapters_dialog, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { dialog, _ ->
-                    val view = dialog.customView
-                    if (view != null) {
-                        // Remove focus to update selected number
-                        val np: NumberPicker = view.findViewById(R.id.chapters_picker)
-                        np.clearFocus()
-
-                        (targetController as? Listener)?.setChaptersRead(item, np.value)
-                    }
-                }
-                .build()
-
-        val view = dialog.customView
-        if (view != null) {
-            val np: NumberPicker = view.findViewById(R.id.chapters_picker)
-            // Set initial value
-            np.value = item.track?.last_chapter_read ?: 0
-            // Don't allow to go from 0 to 9999
-            np.wrapSelectorWheel = false
-        }
-
-        return dialog
-    }
-
-    interface Listener {
-        fun setChaptersRead(item: TrackItem, chaptersRead: Int)
-    }
-
-    private companion object {
-        const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track"
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.widget.NumberPicker
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackChaptersDialog<T> : DialogController
+        where T : Controller, T : SetTrackChaptersDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+
+        val dialog = MaterialDialog.Builder(activity!!)
+                .title(R.string.chapters)
+                .customView(R.layout.track_chapters_dialog, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { dialog, _ ->
+                    val view = dialog.customView
+                    if (view != null) {
+                        // Remove focus to update selected number
+                        val np: NumberPicker = view.findViewById(R.id.chapters_picker)
+                        np.clearFocus()
+
+                        (targetController as? Listener)?.setChaptersRead(item, np.value)
+                    }
+                }
+                .build()
+
+        val view = dialog.customView
+        if (view != null) {
+            val np: NumberPicker = view.findViewById(R.id.chapters_picker)
+            // Set initial value
+            np.value = item.track?.last_chapter_read ?: 0
+            // Don't allow to go from 0 to 9999
+            np.wrapSelectorWheel = false
+        }
+
+        return dialog
+    }
+
+    interface Listener {
+        fun setChaptersRead(item: TrackItem, chaptersRead: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track"
+    }
+
 }

+ 79 - 79
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt

@@ -1,80 +1,80 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.app.Dialog
-import android.os.Bundle
-import android.widget.NumberPicker
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class SetTrackScoreDialog<T> : DialogController
-        where T : Controller, T : SetTrackScoreDialog.Listener {
-
-    private val item: TrackItem
-
-    constructor(target: T, item: TrackItem) : super(Bundle().apply {
-        putSerializable(KEY_ITEM_TRACK, item.track)
-    }) {
-        targetController = target
-        this.item = item
-    }
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : super(bundle) {
-        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
-        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
-        item = TrackItem(track, service)
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val item = item
-
-        val dialog = MaterialDialog.Builder(activity!!)
-                .title(R.string.score)
-                .customView(R.layout.track_score_dialog, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { dialog, _ ->
-                    val view = dialog.customView
-                    if (view != null) {
-                        // Remove focus to update selected number
-                        val np: NumberPicker = view.findViewById(R.id.score_picker)
-                        np.clearFocus()
-
-                        (targetController as? Listener)?.setScore(item, np.value)
-                    }
-                }
-                .show()
-
-        val view = dialog.customView
-        if (view != null) {
-            val np: NumberPicker = view.findViewById(R.id.score_picker)
-            val scores = item.service.getScoreList().toTypedArray()
-            np.maxValue = scores.size - 1
-            np.displayedValues = scores
-
-            // Set initial value
-            val displayedScore = item.service.displayScore(item.track!!)
-            if (displayedScore != "-") {
-                val index = scores.indexOf(displayedScore)
-                np.value = if (index != -1) index else 0
-            }
-        }
-
-        return dialog
-    }
-
-    interface Listener {
-        fun setScore(item: TrackItem, score: Int)
-    }
-
-    private companion object {
-        const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track"
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.widget.NumberPicker
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackScoreDialog<T> : DialogController
+        where T : Controller, T : SetTrackScoreDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+
+        val dialog = MaterialDialog.Builder(activity!!)
+                .title(R.string.score)
+                .customView(R.layout.track_score_dialog, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { dialog, _ ->
+                    val view = dialog.customView
+                    if (view != null) {
+                        // Remove focus to update selected number
+                        val np: NumberPicker = view.findViewById(R.id.score_picker)
+                        np.clearFocus()
+
+                        (targetController as? Listener)?.setScore(item, np.value)
+                    }
+                }
+                .show()
+
+        val view = dialog.customView
+        if (view != null) {
+            val np: NumberPicker = view.findViewById(R.id.score_picker)
+            val scores = item.service.getScoreList().toTypedArray()
+            np.maxValue = scores.size - 1
+            np.displayedValues = scores
+
+            // Set initial value
+            val displayedScore = item.service.displayScore(item.track!!)
+            if (displayedScore != "-") {
+                val index = scores.indexOf(displayedScore)
+                np.value = if (index != -1) index else 0
+            }
+        }
+
+        return dialog
+    }
+
+    interface Listener {
+        fun setScore(item: TrackItem, score: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track"
+    }
+
 }

+ 57 - 57
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt

@@ -1,58 +1,58 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.app.Dialog
-import android.os.Bundle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bluelinelabs.conductor.Controller
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class SetTrackStatusDialog<T> : DialogController
-        where T : Controller, T : SetTrackStatusDialog.Listener {
-
-    private val item: TrackItem
-
-    constructor(target: T, item: TrackItem) : super(Bundle().apply {
-        putSerializable(KEY_ITEM_TRACK, item.track)
-    }) {
-        targetController = target
-        this.item = item
-    }
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : super(bundle) {
-        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
-        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
-        item = TrackItem(track, service)
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val item = item
-        val statusList = item.service.getStatusList().orEmpty()
-        val statusString = statusList.mapNotNull { item.service.getStatus(it) }
-        val selectedIndex = statusList.indexOf(item.track?.status)
-
-        return MaterialDialog.Builder(activity!!)
-                .title(R.string.status)
-                .negativeText(android.R.string.cancel)
-                .items(statusString)
-                .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ ->
-                    (targetController as? Listener)?.setStatus(item, i)
-                    true
-                })
-                .build()
-    }
-
-    interface Listener {
-        fun setStatus(item: TrackItem, selection: Int)
-    }
-
-    private companion object {
-        const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track"
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackStatusDialog<T> : DialogController
+        where T : Controller, T : SetTrackStatusDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+        val statusList = item.service.getStatusList().orEmpty()
+        val statusString = statusList.mapNotNull { item.service.getStatus(it) }
+        val selectedIndex = statusList.indexOf(item.track?.status)
+
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.status)
+                .negativeText(android.R.string.cancel)
+                .items(statusString)
+                .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ ->
+                    (targetController as? Listener)?.setStatus(item, i)
+                    true
+                })
+                .build()
+    }
+
+    interface Listener {
+        fun setStatus(item: TrackItem, selection: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track"
+    }
+
 }

+ 45 - 45
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt

@@ -1,45 +1,45 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.support.v7.widget.RecyclerView
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.inflate
-
-class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
-
-    var items = emptyList<TrackItem>()
-        set(value) {
-            if (field !== value) {
-                field = value
-                notifyDataSetChanged()
-            }
-        }
-
-    val rowClickListener: OnClickListener = controller
-
-    fun getItem(index: Int): TrackItem? {
-        return items.getOrNull(index)
-    }
-
-    override fun getItemCount(): Int {
-        return items.size
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
-        val view = parent.inflate(R.layout.track_item)
-        return TrackHolder(view, this)
-    }
-
-    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
-        holder.bind(items[position])
-    }
-
-    interface OnClickListener {
-        fun onLogoClick(position: Int)
-        fun onTitleClick(position: Int)
-        fun onStatusClick(position: Int)
-        fun onChaptersClick(position: Int)
-        fun onScoreClick(position: Int)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.inflate
+
+class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
+
+    var items = emptyList<TrackItem>()
+        set(value) {
+            if (field !== value) {
+                field = value
+                notifyDataSetChanged()
+            }
+        }
+
+    val rowClickListener: OnClickListener = controller
+
+    fun getItem(index: Int): TrackItem? {
+        return items.getOrNull(index)
+    }
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
+        val view = parent.inflate(R.layout.track_item)
+        return TrackHolder(view, this)
+    }
+
+    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
+        holder.bind(items[position])
+    }
+
+    interface OnClickListener {
+        fun onLogoClick(position: Int)
+        fun onTitleClick(position: Int)
+        fun onStatusClick(position: Int)
+        fun onChaptersClick(position: Int)
+        fun onScoreClick(position: Int)
+    }
+
+}

+ 141 - 141
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt

@@ -1,142 +1,142 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.content.Intent
-import android.net.Uri
-import android.support.v7.widget.LinearLayoutManager
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.track_controller.*
-import timber.log.Timber
-
-class TrackController : NucleusController<TrackPresenter>(),
-        TrackAdapter.OnClickListener,
-        SetTrackStatusDialog.Listener,
-        SetTrackChaptersDialog.Listener,
-        SetTrackScoreDialog.Listener {
-
-    private var adapter: TrackAdapter? = null
-
-    init {
-        // There's no menu, but this avoids a bug when coming from the catalogue, where the menu
-        // disappears if the searchview is expanded
-        setHasOptionsMenu(true)
-    }
-
-    override fun createPresenter(): TrackPresenter {
-        return TrackPresenter((parentController as MangaController).manga!!)
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.track_controller, container, false)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        adapter = TrackAdapter(this)
-        with(view) {
-            track_recycler.layoutManager = LinearLayoutManager(context)
-            track_recycler.adapter = adapter
-            swipe_refresh.isEnabled = false
-            swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        adapter = null
-        super.onDestroyView(view)
-    }
-
-    fun onNextTrackings(trackings: List<TrackItem>) {
-        val atLeastOneLink = trackings.any { it.track != null }
-        adapter?.items = trackings
-        swipe_refresh?.isEnabled = atLeastOneLink
-        (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
-    }
-
-    fun onSearchResults(results: List<TrackSearch>) {
-        getSearchDialog()?.onSearchResults(results)
-    }
-
-    @Suppress("UNUSED_PARAMETER")
-    fun onSearchResultsError(error: Throwable) {
-        Timber.e(error)
-        getSearchDialog()?.onSearchResultsError()
-    }
-
-    private fun getSearchDialog(): TrackSearchDialog? {
-        return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
-    }
-
-    fun onRefreshDone() {
-        swipe_refresh?.isRefreshing = false
-    }
-
-    fun onRefreshError(error: Throwable) {
-        swipe_refresh?.isRefreshing = false
-        activity?.toast(error.message)
-    }
-
-    override fun onLogoClick(position: Int) {
-        val track = adapter?.getItem(position)?.track ?: return
-
-        if (track.tracking_url.isNullOrBlank()) {
-            activity?.toast(R.string.url_not_set)
-        } else {
-            activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
-        }
-    }
-
-    override fun onTitleClick(position: Int) {
-        val item = adapter?.getItem(position) ?: return
-        TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
-    }
-
-    override fun onStatusClick(position: Int) {
-        val item = adapter?.getItem(position) ?: return
-        if (item.track == null) return
-
-        SetTrackStatusDialog(this, item).showDialog(router)
-    }
-
-    override fun onChaptersClick(position: Int) {
-        val item = adapter?.getItem(position) ?: return
-        if (item.track == null) return
-
-        SetTrackChaptersDialog(this, item).showDialog(router)
-    }
-
-    override fun onScoreClick(position: Int) {
-        val item = adapter?.getItem(position) ?: return
-        if (item.track == null) return
-
-        SetTrackScoreDialog(this, item).showDialog(router)
-    }
-
-    override fun setStatus(item: TrackItem, selection: Int) {
-        presenter.setStatus(item, selection)
-        swipe_refresh?.isRefreshing = true
-    }
-
-    override fun setScore(item: TrackItem, score: Int) {
-        presenter.setScore(item, score)
-        swipe_refresh?.isRefreshing = true
-    }
-
-    override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
-        presenter.setLastChapterRead(item, chaptersRead)
-        swipe_refresh?.isRefreshing = true
-    }
-
-    private companion object {
-        const val TAG_SEARCH_CONTROLLER = "track_search_controller"
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.content.Intent
+import android.net.Uri
+import android.support.v7.widget.LinearLayoutManager
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.track_controller.*
+import timber.log.Timber
+
+class TrackController : NucleusController<TrackPresenter>(),
+        TrackAdapter.OnClickListener,
+        SetTrackStatusDialog.Listener,
+        SetTrackChaptersDialog.Listener,
+        SetTrackScoreDialog.Listener {
+
+    private var adapter: TrackAdapter? = null
+
+    init {
+        // There's no menu, but this avoids a bug when coming from the catalogue, where the menu
+        // disappears if the searchview is expanded
+        setHasOptionsMenu(true)
+    }
+
+    override fun createPresenter(): TrackPresenter {
+        return TrackPresenter((parentController as MangaController).manga!!)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.track_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        adapter = TrackAdapter(this)
+        with(view) {
+            track_recycler.layoutManager = LinearLayoutManager(context)
+            track_recycler.adapter = adapter
+            swipe_refresh.isEnabled = false
+            swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        adapter = null
+        super.onDestroyView(view)
+    }
+
+    fun onNextTrackings(trackings: List<TrackItem>) {
+        val atLeastOneLink = trackings.any { it.track != null }
+        adapter?.items = trackings
+        swipe_refresh?.isEnabled = atLeastOneLink
+        (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
+    }
+
+    fun onSearchResults(results: List<TrackSearch>) {
+        getSearchDialog()?.onSearchResults(results)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    fun onSearchResultsError(error: Throwable) {
+        Timber.e(error)
+        getSearchDialog()?.onSearchResultsError()
+    }
+
+    private fun getSearchDialog(): TrackSearchDialog? {
+        return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
+    }
+
+    fun onRefreshDone() {
+        swipe_refresh?.isRefreshing = false
+    }
+
+    fun onRefreshError(error: Throwable) {
+        swipe_refresh?.isRefreshing = false
+        activity?.toast(error.message)
+    }
+
+    override fun onLogoClick(position: Int) {
+        val track = adapter?.getItem(position)?.track ?: return
+
+        if (track.tracking_url.isNullOrBlank()) {
+            activity?.toast(R.string.url_not_set)
+        } else {
+            activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
+        }
+    }
+
+    override fun onTitleClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
+    }
+
+    override fun onStatusClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackStatusDialog(this, item).showDialog(router)
+    }
+
+    override fun onChaptersClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackChaptersDialog(this, item).showDialog(router)
+    }
+
+    override fun onScoreClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackScoreDialog(this, item).showDialog(router)
+    }
+
+    override fun setStatus(item: TrackItem, selection: Int) {
+        presenter.setStatus(item, selection)
+        swipe_refresh?.isRefreshing = true
+    }
+
+    override fun setScore(item: TrackItem, score: Int) {
+        presenter.setScore(item, score)
+        swipe_refresh?.isRefreshing = true
+    }
+
+    override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
+        presenter.setLastChapterRead(item, chaptersRead)
+        swipe_refresh?.isRefreshing = true
+    }
+
+    private companion object {
+        const val TAG_SEARCH_CONTROLLER = "track_search_controller"
+    }
+
 }

+ 42 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt

@@ -1,42 +1,42 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.annotation.SuppressLint
-import android.view.View
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
-import kotlinx.android.synthetic.main.track_item.*
-
-class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
-
-    init {
-        val listener = adapter.rowClickListener
-        logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
-        title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
-        status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
-        chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
-        score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
-    }
-
-    @SuppressLint("SetTextI18n")
-    @Suppress("DEPRECATION")
-    fun bind(item: TrackItem) {
-        val track = item.track
-        track_logo.setImageResource(item.service.getLogo())
-        logo_container.setBackgroundColor(item.service.getLogoColor())
-        if (track != null) {
-            track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
-            track_title.setAllCaps(false)
-            track_title.text = track.title
-            track_chapters.text = "${track.last_chapter_read}/" +
-                    if (track.total_chapters > 0) track.total_chapters else "-"
-            track_status.text = item.service.getStatus(track.status)
-            track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
-        } else {
-            track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
-            track_title.setText(R.string.action_edit)
-            track_chapters.text = ""
-            track_score.text = ""
-            track_status.text = ""
-        }
-    }
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.annotation.SuppressLint
+import android.view.View
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
+import kotlinx.android.synthetic.main.track_item.*
+
+class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
+
+    init {
+        val listener = adapter.rowClickListener
+        logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
+        title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
+        status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
+        chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
+        score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
+    }
+
+    @SuppressLint("SetTextI18n")
+    @Suppress("DEPRECATION")
+    fun bind(item: TrackItem) {
+        val track = item.track
+        track_logo.setImageResource(item.service.getLogo())
+        logo_container.setBackgroundColor(item.service.getLogoColor())
+        if (track != null) {
+            track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
+            track_title.setAllCaps(false)
+            track_title.text = track.title
+            track_chapters.text = "${track.last_chapter_read}/" +
+                    if (track.total_chapters > 0) track.total_chapters else "-"
+            track_status.text = item.service.getStatus(track.status)
+            track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
+        } else {
+            track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
+            track_title.setText(R.string.action_edit)
+            track_chapters.text = ""
+            track_score.text = ""
+            track_status.text = ""
+        }
+    }
+}

+ 6 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt

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

+ 129 - 129
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt

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

+ 78 - 78
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt

@@ -1,79 +1,79 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.content.Context
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ArrayAdapter
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import eu.kanade.tachiyomi.util.gone
-import eu.kanade.tachiyomi.util.inflate
-import kotlinx.android.synthetic.main.track_search_item.view.*
-import java.util.*
-
-class TrackSearchAdapter(context: Context)
-    : ArrayAdapter<TrackSearch>(context, R.layout.track_search_item, ArrayList<TrackSearch>()) {
-
-    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
-        var v = view
-        // Get the data item for this position
-        val track = getItem(position)
-        // Check if an existing view is being reused, otherwise inflate the view
-        val holder: TrackSearchHolder // view lookup cache stored in tag
-        if (v == null) {
-            v = parent.inflate(R.layout.track_search_item)
-            holder = TrackSearchHolder(v)
-            v.tag = holder
-        } else {
-            holder = v.tag as TrackSearchHolder
-        }
-        holder.onSetValues(track)
-        return v
-    }
-
-    fun setItems(syncs: List<TrackSearch>) {
-        setNotifyOnChange(false)
-        clear()
-        addAll(syncs)
-        notifyDataSetChanged()
-    }
-
-    class TrackSearchHolder(private val view: View) {
-
-        fun onSetValues(track: TrackSearch) {
-            view.track_search_title.text = track.title
-            view.track_search_summary.text = track.summary
-            GlideApp.with(view.context).clear(view.track_search_cover)
-            if (!track.cover_url.isNullOrEmpty()) {
-                GlideApp.with(view.context)
-                        .load(track.cover_url)
-                        .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                        .centerCrop()
-                        .into(view.track_search_cover)
-            }
-
-            if (track.publishing_status.isNullOrBlank()) {
-                view.track_search_status.gone()
-                view.track_search_status_result.gone()
-            } else {
-                view.track_search_status_result.text = track.publishing_status.capitalize()
-            }
-
-            if (track.publishing_type.isNullOrBlank()) {
-                view.track_search_type.gone()
-                view.track_search_type_result.gone()
-            } else {
-                view.track_search_type_result.text = track.publishing_type.capitalize()
-            }
-
-            if (track.start_date.isNullOrBlank()) {
-                view.track_search_start.gone()
-                view.track_search_start_result.gone()
-            } else {
-                view.track_search_start_result.text = track.start_date
-            }
-        }
-    }
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.inflate
+import kotlinx.android.synthetic.main.track_search_item.view.*
+import java.util.*
+
+class TrackSearchAdapter(context: Context)
+    : ArrayAdapter<TrackSearch>(context, R.layout.track_search_item, ArrayList<TrackSearch>()) {
+
+    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
+        var v = view
+        // Get the data item for this position
+        val track = getItem(position)
+        // Check if an existing view is being reused, otherwise inflate the view
+        val holder: TrackSearchHolder // view lookup cache stored in tag
+        if (v == null) {
+            v = parent.inflate(R.layout.track_search_item)
+            holder = TrackSearchHolder(v)
+            v.tag = holder
+        } else {
+            holder = v.tag as TrackSearchHolder
+        }
+        holder.onSetValues(track)
+        return v
+    }
+
+    fun setItems(syncs: List<TrackSearch>) {
+        setNotifyOnChange(false)
+        clear()
+        addAll(syncs)
+        notifyDataSetChanged()
+    }
+
+    class TrackSearchHolder(private val view: View) {
+
+        fun onSetValues(track: TrackSearch) {
+            view.track_search_title.text = track.title
+            view.track_search_summary.text = track.summary
+            GlideApp.with(view.context).clear(view.track_search_cover)
+            if (!track.cover_url.isNullOrEmpty()) {
+                GlideApp.with(view.context)
+                        .load(track.cover_url)
+                        .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+                        .centerCrop()
+                        .into(view.track_search_cover)
+            }
+
+            if (track.publishing_status.isNullOrBlank()) {
+                view.track_search_status.gone()
+                view.track_search_status_result.gone()
+            } else {
+                view.track_search_status_result.text = track.publishing_status.capitalize()
+            }
+
+            if (track.publishing_type.isNullOrBlank()) {
+                view.track_search_type.gone()
+                view.track_search_type_result.gone()
+            } else {
+                view.track_search_type_result.text = track.publishing_type.capitalize()
+            }
+
+            if (track.start_date.isNullOrBlank()) {
+                view.track_search_start.gone()
+                view.track_search_start_result.gone()
+            } else {
+                view.track_search_start_result.text = track.start_date
+            }
+        }
+    }
 }

+ 144 - 144
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt

@@ -1,144 +1,144 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.app.Dialog
-import android.os.Bundle
-import android.view.View
-import com.afollestad.materialdialogs.MaterialDialog
-import com.jakewharton.rxbinding.widget.itemClicks
-import com.jakewharton.rxbinding.widget.textChanges
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.data.track.model.TrackSearch
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.util.plusAssign
-import kotlinx.android.synthetic.main.track_search_dialog.view.*
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.subscriptions.CompositeSubscription
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.util.concurrent.TimeUnit
-
-class TrackSearchDialog : DialogController {
-
-    private var dialogView: View? = null
-
-    private var adapter: TrackSearchAdapter? = null
-
-    private var selectedItem: Track? = null
-
-    private val service: TrackService
-
-    private var subscriptions = CompositeSubscription()
-
-    private var searchTextSubscription: Subscription? = null
-
-    private val trackController
-        get() = targetController as TrackController
-
-    constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
-        putInt(KEY_SERVICE, service.id)
-    }) {
-        targetController = target
-        this.service = service
-    }
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : super(bundle) {
-        service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
-    }
-
-    override fun onCreateDialog(savedState: Bundle?): Dialog {
-        val dialog = MaterialDialog.Builder(activity!!)
-                .customView(R.layout.track_search_dialog, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { _, _ -> onPositiveButtonClick() }
-                .build()
-
-        if (subscriptions.isUnsubscribed) {
-            subscriptions = CompositeSubscription()
-        }
-
-        dialogView = dialog.view
-        onViewCreated(dialog.view, savedState)
-
-        return dialog
-    }
-
-    fun onViewCreated(view: View, savedState: Bundle?) {
-        // Create adapter
-        val adapter = TrackSearchAdapter(view.context)
-        this.adapter = adapter
-        view.track_search_list.adapter = adapter
-
-        // Set listeners
-        selectedItem = null
-
-        subscriptions += view.track_search_list.itemClicks().subscribe { position ->
-            selectedItem = adapter.getItem(position)
-        }
-
-        // Do an initial search based on the manga's title
-        if (savedState == null) {
-            val title = trackController.presenter.manga.title
-            view.track_search.append(title)
-            search(title)
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        super.onDestroyView(view)
-        subscriptions.unsubscribe()
-        dialogView = null
-        adapter = null
-    }
-
-    override fun onAttach(view: View) {
-        super.onAttach(view)
-        searchTextSubscription = dialogView!!.track_search.textChanges()
-                .skip(1)
-                .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
-                .map { it.toString() }
-                .filter(String::isNotBlank)
-                .subscribe { search(it) }
-    }
-
-    override fun onDetach(view: View) {
-        super.onDetach(view)
-        searchTextSubscription?.unsubscribe()
-    }
-
-    private fun search(query: String) {
-        val view = dialogView ?: return
-        view.progress.visibility = View.VISIBLE
-        view.track_search_list.visibility = View.INVISIBLE
-        trackController.presenter.search(query, service)
-    }
-
-    fun onSearchResults(results: List<TrackSearch>) {
-        selectedItem = null
-        val view = dialogView ?: return
-        view.progress.visibility = View.INVISIBLE
-        view.track_search_list.visibility = View.VISIBLE
-        adapter?.setItems(results)
-    }
-
-    fun onSearchResultsError() {
-        val view = dialogView ?: return
-        view.progress.visibility = View.VISIBLE
-        view.track_search_list.visibility = View.INVISIBLE
-        adapter?.setItems(emptyList())
-    }
-
-    private fun onPositiveButtonClick() {
-        trackController.presenter.registerTracking(selectedItem, service)
-    }
-
-    private companion object {
-        const val KEY_SERVICE = "service_id"
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.View
+import com.afollestad.materialdialogs.MaterialDialog
+import com.jakewharton.rxbinding.widget.itemClicks
+import com.jakewharton.rxbinding.widget.textChanges
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.util.plusAssign
+import kotlinx.android.synthetic.main.track_search_dialog.view.*
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.subscriptions.CompositeSubscription
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.concurrent.TimeUnit
+
+class TrackSearchDialog : DialogController {
+
+    private var dialogView: View? = null
+
+    private var adapter: TrackSearchAdapter? = null
+
+    private var selectedItem: Track? = null
+
+    private val service: TrackService
+
+    private var subscriptions = CompositeSubscription()
+
+    private var searchTextSubscription: Subscription? = null
+
+    private val trackController
+        get() = targetController as TrackController
+
+    constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
+        putInt(KEY_SERVICE, service.id)
+    }) {
+        targetController = target
+        this.service = service
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
+    }
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        val dialog = MaterialDialog.Builder(activity!!)
+                .customView(R.layout.track_search_dialog, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { _, _ -> onPositiveButtonClick() }
+                .build()
+
+        if (subscriptions.isUnsubscribed) {
+            subscriptions = CompositeSubscription()
+        }
+
+        dialogView = dialog.view
+        onViewCreated(dialog.view, savedState)
+
+        return dialog
+    }
+
+    fun onViewCreated(view: View, savedState: Bundle?) {
+        // Create adapter
+        val adapter = TrackSearchAdapter(view.context)
+        this.adapter = adapter
+        view.track_search_list.adapter = adapter
+
+        // Set listeners
+        selectedItem = null
+
+        subscriptions += view.track_search_list.itemClicks().subscribe { position ->
+            selectedItem = adapter.getItem(position)
+        }
+
+        // Do an initial search based on the manga's title
+        if (savedState == null) {
+            val title = trackController.presenter.manga.title
+            view.track_search.append(title)
+            search(title)
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        subscriptions.unsubscribe()
+        dialogView = null
+        adapter = null
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        searchTextSubscription = dialogView!!.track_search.textChanges()
+                .skip(1)
+                .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
+                .map { it.toString() }
+                .filter(String::isNotBlank)
+                .subscribe { search(it) }
+    }
+
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+        searchTextSubscription?.unsubscribe()
+    }
+
+    private fun search(query: String) {
+        val view = dialogView ?: return
+        view.progress.visibility = View.VISIBLE
+        view.track_search_list.visibility = View.INVISIBLE
+        trackController.presenter.search(query, service)
+    }
+
+    fun onSearchResults(results: List<TrackSearch>) {
+        selectedItem = null
+        val view = dialogView ?: return
+        view.progress.visibility = View.INVISIBLE
+        view.track_search_list.visibility = View.VISIBLE
+        adapter?.setItems(results)
+    }
+
+    fun onSearchResultsError() {
+        val view = dialogView ?: return
+        view.progress.visibility = View.VISIBLE
+        view.track_search_list.visibility = View.INVISIBLE
+        adapter?.setItems(emptyList())
+    }
+
+    private fun onPositiveButtonClick() {
+        trackController.presenter.registerTracking(selectedItem, service)
+    }
+
+    private companion object {
+        const val KEY_SERVICE = "service_id"
+    }
+
+}

+ 333 - 333
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt

@@ -1,333 +1,333 @@
-package eu.kanade.tachiyomi.ui.recent_updates
-
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.DividerItemDecoration
-import android.support.v7.widget.LinearLayoutManager
-import android.view.*
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.SelectableAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
-import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.recent_chapters_controller.*
-import timber.log.Timber
-
-/**
- * Fragment that shows recent chapters.
- * Uses [R.layout.recent_chapters_controller].
- * UI related actions should be called from here.
- */
-class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
-        NoToolbarElevationController,
-        ActionMode.Callback,
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener,
-        FlexibleAdapter.OnUpdateListener,
-        ConfirmDeleteChaptersDialog.Listener,
-        RecentChaptersAdapter.OnCoverClickListener {
-
-    /**
-     * Action mode for multiple selection.
-     */
-    private var actionMode: ActionMode? = null
-
-    /**
-     * Adapter containing the recent chapters.
-     */
-    var adapter: RecentChaptersAdapter? = null
-        private set
-
-    override fun getTitle(): String? {
-        return resources?.getString(R.string.label_recent_updates)
-    }
-
-    override fun createPresenter(): RecentChaptersPresenter {
-        return RecentChaptersPresenter()
-    }
-
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.recent_chapters_controller, container, false)
-    }
-
-    /**
-     * Called when view is created
-     * @param view created view
-     */
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        // Init RecyclerView and adapter
-        val layoutManager = LinearLayoutManager(view.context)
-        recycler.layoutManager = layoutManager
-        recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
-        recycler.setHasFixedSize(true)
-        adapter = RecentChaptersAdapter(this@RecentChaptersController)
-        recycler.adapter = adapter
-
-        recycler.scrollStateChanges().subscribeUntilDestroy {
-            // Disable swipe refresh when view is not at the top
-            val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
-            swipe_refresh.isEnabled = firstPos <= 0
-        }
-
-        swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
-        swipe_refresh.refreshes().subscribeUntilDestroy {
-            if (!LibraryUpdateService.isRunning(view.context)) {
-                LibraryUpdateService.start(view.context)
-                view.context.toast(R.string.action_update_library)
-            }
-            // It can be a very long operation, so we disable swipe refresh and show a toast.
-            swipe_refresh.isRefreshing = false
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        adapter = null
-        actionMode = null
-        super.onDestroyView(view)
-    }
-
-    /**
-     * Returns selected chapters
-     * @return list of selected chapters
-     */
-    fun getSelectedChapters(): List<RecentChapterItem> {
-        val adapter = adapter ?: return emptyList()
-        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
-    }
-
-    /**
-     * Called when item in list is clicked
-     * @param position position of clicked item
-     */
-    override fun onItemClick(position: Int): Boolean {
-        val adapter = adapter ?: return false
-
-        // Get item from position
-        val item = adapter.getItem(position) as? RecentChapterItem ?: return false
-        if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openChapter(item)
-            return false
-        }
-    }
-
-    /**
-     * Called when item in list is long clicked
-     * @param position position of clicked item
-     */
-    override fun onItemLongClick(position: Int) {
-        if (actionMode == null)
-            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
-
-        toggleSelection(position)
-    }
-
-    /**
-     * Called to toggle selection
-     * @param position position of selected item
-     */
-    private fun toggleSelection(position: Int) {
-        val adapter = adapter ?: return
-        adapter.toggleSelection(position)
-        actionMode?.invalidate()
-    }
-
-    /**
-     * Open chapter in reader
-     * @param chapter selected chapter
-     */
-    private fun openChapter(item: RecentChapterItem) {
-        val activity = activity ?: return
-        val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
-        startActivity(intent)
-    }
-
-    /**
-     * Download selected items
-     * @param chapters list of selected [RecentChapter]s
-     */
-    fun downloadChapters(chapters: List<RecentChapterItem>) {
-        destroyActionModeIfNeeded()
-        presenter.downloadChapters(chapters)
-    }
-
-    /**
-     * Populate adapter with chapters
-     * @param chapters list of [Any]
-     */
-    fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
-        destroyActionModeIfNeeded()
-        adapter?.updateDataSet(chapters)
-    }
-
-    override fun onUpdateEmptyView(size: Int) {
-        if (size > 0) {
-            empty_view?.hide()
-        } else {
-            empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
-        }
-    }
-
-    /**
-     * Update download status of chapter
-     * @param download [Download] object containing download progress.
-     */
-    fun onChapterStatusChange(download: Download) {
-        getHolder(download)?.notifyStatus(download.status)
-    }
-
-    /**
-     * Returns holder belonging to chapter
-     * @param download [Download] object containing download progress.
-     */
-    private fun getHolder(download: Download): RecentChapterHolder? {
-        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
-    }
-
-    /**
-     * Mark chapter as read
-     * @param chapters list of chapters
-     */
-    fun markAsRead(chapters: List<RecentChapterItem>) {
-        presenter.markChapterRead(chapters, true)
-        if (presenter.preferences.removeAfterMarkedAsRead()) {
-            deleteChapters(chapters)
-        }
-    }
-
-    override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) {
-        destroyActionModeIfNeeded()
-        DeletingChaptersDialog().showDialog(router)
-        presenter.deleteChapters(chaptersToDelete)
-    }
-
-    /**
-     * Destory [ActionMode] if it's shown
-     */
-    fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    /**
-     * Mark chapter as unread
-     * @param chapters list of selected [RecentChapter]
-     */
-    fun markAsUnread(chapters: List<RecentChapterItem>) {
-        presenter.markChapterRead(chapters, false)
-    }
-
-    /**
-     * Start downloading chapter
-     * @param chapter selected chapter with manga
-     */
-    fun downloadChapter(chapter: RecentChapterItem) {
-        presenter.downloadChapters(listOf(chapter))
-    }
-
-    /**
-     * Start deleting chapter
-     * @param chapter selected chapter with manga
-     */
-    fun deleteChapter(chapter: RecentChapterItem) {
-        DeletingChaptersDialog().showDialog(router)
-        presenter.deleteChapters(listOf(chapter))
-    }
-
-    override fun onCoverClick(position: Int) {
-        val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return
-        openManga(chapterClicked)
-
-    }
-
-    fun openManga(chapter: RecentChapterItem) {
-        router.pushController(MangaController(chapter.manga).withFadeTransaction())
-    }
-
-    /**
-     * Called when chapters are deleted
-     */
-    fun onChaptersDeleted() {
-        dismissDeletingDialog()
-        adapter?.notifyDataSetChanged()
-    }
-
-    /**
-     * Called when error while deleting
-     * @param error error message
-     */
-    fun onChaptersDeletedError(error: Throwable) {
-        dismissDeletingDialog()
-        Timber.e(error)
-    }
-
-    /**
-     * Called to dismiss deleting dialog
-     */
-    fun dismissDeletingDialog() {
-        router.popControllerWithTag(DeletingChaptersDialog.TAG)
-    }
-
-    /**
-     * Called when ActionMode created.
-     * @param mode the ActionMode object
-     * @param menu menu object of ActionMode
-     */
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
-        adapter?.mode = SelectableAdapter.Mode.MULTI
-        return true
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = adapter?.selectedItemCount ?: 0
-        if (count == 0) {
-            // Destroy action mode if there are no items selected.
-            destroyActionModeIfNeeded()
-        } else {
-            mode.title = resources?.getString(R.string.label_selected, count)
-        }
-        return false
-    }
-
-    /**
-     * Called when ActionMode item clicked
-     * @param mode the ActionMode object
-     * @param item item from ActionMode.
-     */
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
-            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
-            R.id.action_download -> downloadChapters(getSelectedChapters())
-            R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters())
-                    .showDialog(router)
-            else -> return false
-        }
-        return true
-    }
-
-    /**
-     * Called when ActionMode destroyed
-     * @param mode the ActionMode object
-     */
-    override fun onDestroyActionMode(mode: ActionMode?) {
-        adapter?.mode = SelectableAdapter.Mode.IDLE
-        adapter?.clearSelection()
-        actionMode = null
-    }
-
-}
+package eu.kanade.tachiyomi.ui.recent_updates
+
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.DividerItemDecoration
+import android.support.v7.widget.LinearLayoutManager
+import android.view.*
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.recent_chapters_controller.*
+import timber.log.Timber
+
+/**
+ * Fragment that shows recent chapters.
+ * Uses [R.layout.recent_chapters_controller].
+ * UI related actions should be called from here.
+ */
+class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
+        NoToolbarElevationController,
+        ActionMode.Callback,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        FlexibleAdapter.OnUpdateListener,
+        ConfirmDeleteChaptersDialog.Listener,
+        RecentChaptersAdapter.OnCoverClickListener {
+
+    /**
+     * Action mode for multiple selection.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Adapter containing the recent chapters.
+     */
+    var adapter: RecentChaptersAdapter? = null
+        private set
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_recent_updates)
+    }
+
+    override fun createPresenter(): RecentChaptersPresenter {
+        return RecentChaptersPresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.recent_chapters_controller, container, false)
+    }
+
+    /**
+     * Called when view is created
+     * @param view created view
+     */
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        // Init RecyclerView and adapter
+        val layoutManager = LinearLayoutManager(view.context)
+        recycler.layoutManager = layoutManager
+        recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
+        recycler.setHasFixedSize(true)
+        adapter = RecentChaptersAdapter(this@RecentChaptersController)
+        recycler.adapter = adapter
+
+        recycler.scrollStateChanges().subscribeUntilDestroy {
+            // Disable swipe refresh when view is not at the top
+            val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
+            swipe_refresh.isEnabled = firstPos <= 0
+        }
+
+        swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
+        swipe_refresh.refreshes().subscribeUntilDestroy {
+            if (!LibraryUpdateService.isRunning(view.context)) {
+                LibraryUpdateService.start(view.context)
+                view.context.toast(R.string.action_update_library)
+            }
+            // It can be a very long operation, so we disable swipe refresh and show a toast.
+            swipe_refresh.isRefreshing = false
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        adapter = null
+        actionMode = null
+        super.onDestroyView(view)
+    }
+
+    /**
+     * Returns selected chapters
+     * @return list of selected chapters
+     */
+    fun getSelectedChapters(): List<RecentChapterItem> {
+        val adapter = adapter ?: return emptyList()
+        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
+    }
+
+    /**
+     * Called when item in list is clicked
+     * @param position position of clicked item
+     */
+    override fun onItemClick(position: Int): Boolean {
+        val adapter = adapter ?: return false
+
+        // Get item from position
+        val item = adapter.getItem(position) as? RecentChapterItem ?: return false
+        if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openChapter(item)
+            return false
+        }
+    }
+
+    /**
+     * Called when item in list is long clicked
+     * @param position position of clicked item
+     */
+    override fun onItemLongClick(position: Int) {
+        if (actionMode == null)
+            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
+
+        toggleSelection(position)
+    }
+
+    /**
+     * Called to toggle selection
+     * @param position position of selected item
+     */
+    private fun toggleSelection(position: Int) {
+        val adapter = adapter ?: return
+        adapter.toggleSelection(position)
+        actionMode?.invalidate()
+    }
+
+    /**
+     * Open chapter in reader
+     * @param chapter selected chapter
+     */
+    private fun openChapter(item: RecentChapterItem) {
+        val activity = activity ?: return
+        val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
+        startActivity(intent)
+    }
+
+    /**
+     * Download selected items
+     * @param chapters list of selected [RecentChapter]s
+     */
+    fun downloadChapters(chapters: List<RecentChapterItem>) {
+        destroyActionModeIfNeeded()
+        presenter.downloadChapters(chapters)
+    }
+
+    /**
+     * Populate adapter with chapters
+     * @param chapters list of [Any]
+     */
+    fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
+        destroyActionModeIfNeeded()
+        adapter?.updateDataSet(chapters)
+    }
+
+    override fun onUpdateEmptyView(size: Int) {
+        if (size > 0) {
+            empty_view?.hide()
+        } else {
+            empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
+        }
+    }
+
+    /**
+     * Update download status of chapter
+     * @param download [Download] object containing download progress.
+     */
+    fun onChapterStatusChange(download: Download) {
+        getHolder(download)?.notifyStatus(download.status)
+    }
+
+    /**
+     * Returns holder belonging to chapter
+     * @param download [Download] object containing download progress.
+     */
+    private fun getHolder(download: Download): RecentChapterHolder? {
+        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
+    }
+
+    /**
+     * Mark chapter as read
+     * @param chapters list of chapters
+     */
+    fun markAsRead(chapters: List<RecentChapterItem>) {
+        presenter.markChapterRead(chapters, true)
+        if (presenter.preferences.removeAfterMarkedAsRead()) {
+            deleteChapters(chapters)
+        }
+    }
+
+    override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) {
+        destroyActionModeIfNeeded()
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(chaptersToDelete)
+    }
+
+    /**
+     * Destory [ActionMode] if it's shown
+     */
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    /**
+     * Mark chapter as unread
+     * @param chapters list of selected [RecentChapter]
+     */
+    fun markAsUnread(chapters: List<RecentChapterItem>) {
+        presenter.markChapterRead(chapters, false)
+    }
+
+    /**
+     * Start downloading chapter
+     * @param chapter selected chapter with manga
+     */
+    fun downloadChapter(chapter: RecentChapterItem) {
+        presenter.downloadChapters(listOf(chapter))
+    }
+
+    /**
+     * Start deleting chapter
+     * @param chapter selected chapter with manga
+     */
+    fun deleteChapter(chapter: RecentChapterItem) {
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(listOf(chapter))
+    }
+
+    override fun onCoverClick(position: Int) {
+        val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return
+        openManga(chapterClicked)
+
+    }
+
+    fun openManga(chapter: RecentChapterItem) {
+        router.pushController(MangaController(chapter.manga).withFadeTransaction())
+    }
+
+    /**
+     * Called when chapters are deleted
+     */
+    fun onChaptersDeleted() {
+        dismissDeletingDialog()
+        adapter?.notifyDataSetChanged()
+    }
+
+    /**
+     * Called when error while deleting
+     * @param error error message
+     */
+    fun onChaptersDeletedError(error: Throwable) {
+        dismissDeletingDialog()
+        Timber.e(error)
+    }
+
+    /**
+     * Called to dismiss deleting dialog
+     */
+    fun dismissDeletingDialog() {
+        router.popControllerWithTag(DeletingChaptersDialog.TAG)
+    }
+
+    /**
+     * Called when ActionMode created.
+     * @param mode the ActionMode object
+     * @param menu menu object of ActionMode
+     */
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
+        adapter?.mode = SelectableAdapter.Mode.MULTI
+        return true
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = adapter?.selectedItemCount ?: 0
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+        }
+        return false
+    }
+
+    /**
+     * Called when ActionMode item clicked
+     * @param mode the ActionMode object
+     * @param item item from ActionMode.
+     */
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
+            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
+            R.id.action_download -> downloadChapters(getSelectedChapters())
+            R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters())
+                    .showDialog(router)
+            else -> return false
+        }
+        return true
+    }
+
+    /**
+     * Called when ActionMode destroyed
+     * @param mode the ActionMode object
+     */
+    override fun onDestroyActionMode(mode: ActionMode?) {
+        adapter?.mode = SelectableAdapter.Mode.IDLE
+        adapter?.clearSelection()
+        actionMode = null
+    }
+
+}

+ 86 - 86
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt

@@ -1,87 +1,87 @@
-package eu.kanade.tachiyomi.ui.setting
-
-import android.content.Context
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.preference.PreferenceController
-import android.support.v7.preference.PreferenceScreen
-import android.util.TypedValue
-import android.view.ContextThemeWrapper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.ui.base.controller.BaseController
-import rx.Observable
-import rx.Subscription
-import rx.subscriptions.CompositeSubscription
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-abstract class SettingsController : PreferenceController() {
-
-    val preferences: PreferencesHelper = Injekt.get()
-
-    var untilDestroySubscriptions = CompositeSubscription()
-        private set
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
-        if (untilDestroySubscriptions.isUnsubscribed) {
-            untilDestroySubscriptions = CompositeSubscription()
-        }
-        return super.onCreateView(inflater, container, savedInstanceState)
-    }
-
-    override fun onDestroyView(view: View) {
-        super.onDestroyView(view)
-        untilDestroySubscriptions.unsubscribe()
-    }
-
-    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        val screen = preferenceManager.createPreferenceScreen(getThemedContext())
-        preferenceScreen = screen
-        setupPreferenceScreen(screen)
-    }
-
-    abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any?
-
-    private fun getThemedContext(): Context {
-        val tv = TypedValue()
-        activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
-        return ContextThemeWrapper(activity, tv.resourceId)
-    }
-
-    open fun getTitle(): String? {
-        return preferenceScreen?.title?.toString()
-    }
-
-    fun setTitle() {
-        var parentController = parentController
-        while (parentController != null) {
-            if (parentController is BaseController && parentController.getTitle() != null) {
-                return
-            }
-            parentController = parentController.parentController
-        }
-
-        (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
-    }
-
-    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        if (type.isEnter) {
-            setTitle()
-        }
-        super.onChangeStarted(handler, type)
-    }
-
-    fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
-        return subscribe().also { untilDestroySubscriptions.add(it) }
-    }
-
-    fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
-        return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
-    }
+package eu.kanade.tachiyomi.ui.setting
+
+import android.content.Context
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.preference.PreferenceController
+import android.support.v7.preference.PreferenceScreen
+import android.util.TypedValue
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.controller.BaseController
+import rx.Observable
+import rx.Subscription
+import rx.subscriptions.CompositeSubscription
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+abstract class SettingsController : PreferenceController() {
+
+    val preferences: PreferencesHelper = Injekt.get()
+
+    var untilDestroySubscriptions = CompositeSubscription()
+        private set
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
+        if (untilDestroySubscriptions.isUnsubscribed) {
+            untilDestroySubscriptions = CompositeSubscription()
+        }
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        untilDestroySubscriptions.unsubscribe()
+    }
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        val screen = preferenceManager.createPreferenceScreen(getThemedContext())
+        preferenceScreen = screen
+        setupPreferenceScreen(screen)
+    }
+
+    abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any?
+
+    private fun getThemedContext(): Context {
+        val tv = TypedValue()
+        activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
+        return ContextThemeWrapper(activity, tv.resourceId)
+    }
+
+    open fun getTitle(): String? {
+        return preferenceScreen?.title?.toString()
+    }
+
+    fun setTitle() {
+        var parentController = parentController
+        while (parentController != null) {
+            if (parentController is BaseController && parentController.getTitle() != null) {
+                return
+            }
+            parentController = parentController.parentController
+        }
+
+        (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
+    }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        if (type.isEnter) {
+            setTitle()
+        }
+        super.onChangeStarted(handler, type)
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
+        return subscribe().also { untilDestroySubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
+        return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
+    }
 }

+ 60 - 60
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt

@@ -1,61 +1,61 @@
-package eu.kanade.tachiyomi.ui.setting
-
-import android.support.v7.preference.PreferenceScreen
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
-import eu.kanade.tachiyomi.util.getResourceColor
-
-class SettingsMainController : SettingsController() {
-    override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
-        titleRes = R.string.label_settings
-
-        val tintColor = context.getResourceColor(R.attr.colorAccent)
-
-        preference {
-            iconRes = R.drawable.ic_tune_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_general
-            onClick { navigateTo(SettingsGeneralController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_chrome_reader_mode_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_reader
-            onClick { navigateTo(SettingsReaderController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_file_download_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_downloads
-            onClick { navigateTo(SettingsDownloadController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_sync_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_tracking
-            onClick { navigateTo(SettingsTrackingController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_backup_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.backup
-            onClick { navigateTo(SettingsBackupController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_code_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_advanced
-            onClick { navigateTo(SettingsAdvancedController()) }
-        }
-        preference {
-            iconRes = R.drawable.ic_help_black_24dp
-            iconTint = tintColor
-            titleRes = R.string.pref_category_about
-            onClick { navigateTo(SettingsAboutController()) }
-        }
-    }
-
-    private fun navigateTo(controller: SettingsController) {
-        router.pushController(controller.withFadeTransaction())
-    }
+package eu.kanade.tachiyomi.ui.setting
+
+import android.support.v7.preference.PreferenceScreen
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.util.getResourceColor
+
+class SettingsMainController : SettingsController() {
+    override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
+        titleRes = R.string.label_settings
+
+        val tintColor = context.getResourceColor(R.attr.colorAccent)
+
+        preference {
+            iconRes = R.drawable.ic_tune_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_general
+            onClick { navigateTo(SettingsGeneralController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_chrome_reader_mode_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_reader
+            onClick { navigateTo(SettingsReaderController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_file_download_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_downloads
+            onClick { navigateTo(SettingsDownloadController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_sync_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_tracking
+            onClick { navigateTo(SettingsTrackingController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_backup_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.backup
+            onClick { navigateTo(SettingsBackupController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_code_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_advanced
+            onClick { navigateTo(SettingsAdvancedController()) }
+        }
+        preference {
+            iconRes = R.drawable.ic_help_black_24dp
+            iconTint = tintColor
+            titleRes = R.string.pref_category_about
+            onClick { navigateTo(SettingsAboutController()) }
+        }
+    }
+
+    private fun navigateTo(controller: SettingsController) {
+        router.pushController(controller.withFadeTransaction())
+    }
 }

+ 238 - 238
app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt

@@ -1,239 +1,239 @@
-package eu.kanade.tachiyomi.widget
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.support.annotation.CallSuper
-import android.support.graphics.drawable.VectorDrawableCompat
-import android.support.v4.content.ContextCompat
-import android.support.v7.widget.RecyclerView
-import android.util.AttributeSet
-import android.view.View
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.getResourceColor
-
-/**
- * An alternative implementation of [android.support.design.widget.NavigationView], without menu
- * inflation and allowing customizable items (multiple selections, custom views, etc).
- */
-open class ExtendedNavigationView @JvmOverloads constructor(
-        context: Context,
-        attrs: AttributeSet? = null,
-        defStyleAttr: Int = 0)
-    : SimpleNavigationView(context, attrs, defStyleAttr) {
-
-    /**
-     * Every item of the nav view. Generic items must belong to this list, custom items could be
-     * implemented by an abstract class. If more customization is needed in the future, this can be
-     * changed to an interface instead of sealed class.
-     */
-    sealed class Item {
-        /**
-         * A view separator.
-         */
-        class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
-
-        /**
-         * A header with a title.
-         */
-        class Header(val resTitle: Int) : Item()
-
-        /**
-         * A checkbox.
-         */
-        open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item()
-
-        /**
-         * A checkbox belonging to a group. The group must handle selections and restrictions.
-         */
-        class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false)
-            : Checkbox(resTitle, checked), GroupedItem
-
-        /**
-         * A radio belonging to a group (a sole radio makes no sense). The group must handle
-         * selections and restrictions.
-         */
-        class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false)
-            : Item(), GroupedItem
-
-        /**
-         * An item with which needs more than two states (selected/deselected).
-         */
-        abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() {
-
-            /**
-             * Returns the drawable associated to every possible each state.
-             */
-            abstract fun getStateDrawable(context: Context): Drawable?
-
-            /**
-             * Creates a vector tinted with the accent color.
-             *
-             * @param context any context.
-             * @param resId the vector resource to load and tint
-             */
-            fun tintVector(context: Context, resId: Int): Drawable {
-                return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply {
-                    setTint(context.getResourceColor(R.attr.colorAccent))
-                }
-            }
-        }
-
-        /**
-         * An item with which needs more than two states (selected/deselected) belonging to a group.
-         * The group must handle selections and restrictions.
-         */
-        abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0)
-            : MultiState(resTitle, state), GroupedItem
-
-        /**
-         * A multistate item for sorting lists (unselected, ascending, descending).
-         */
-        class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
-
-            companion object {
-                const val SORT_NONE = 0
-                const val SORT_ASC = 1
-                const val SORT_DESC = 2
-            }
-
-            override fun getStateDrawable(context: Context): Drawable? {
-                return when (state) {
-                    SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp)
-                    SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp)
-                    SORT_NONE -> ContextCompat.getDrawable(context, R.drawable.empty_drawable_32dp)
-                    else -> null
-                }
-            }
-
-        }
-    }
-
-    /**
-     * Interface for an item belonging to a group.
-     */
-    interface GroupedItem {
-        val group: Group
-    }
-
-    /**
-     * A group containing a list of items.
-     */
-    interface Group {
-
-        /**
-         * An optional header for the group, typically a [Item.Header].
-         */
-        val header: Item?
-
-        /**
-         * An optional footer for the group, typically a [Item.Separator].
-         */
-        val footer: Item?
-
-        /**
-         * The items of the group, excluding header and footer.
-         */
-        val items: List<Item>
-
-        /**
-         * Creates all the elements of this group. Implementations can override this method for more
-         * customization.
-         */
-        fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
-
-        /**
-         * Called after creating the list of items. Implementations should load the current values
-         * into the models.
-         */
-        fun initModels()
-
-        /**
-         * Called when an item of this group is clicked. The group is responsible for all the
-         * selections of its items.
-         */
-        fun onItemClicked(item: Item)
-
-    }
-
-    /**
-     * Base adapter for the navigation view. It knows how to create and render every subclass of
-     * [Item].
-     */
-    abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
-
-        private val onClick = View.OnClickListener {
-            val pos = recycler.getChildAdapterPosition(it)
-            val item = items[pos]
-            onItemClicked(item)
-        }
-
-        fun notifyItemChanged(item: Item) {
-            val pos = items.indexOf(item)
-            if (pos != -1) notifyItemChanged(pos)
-        }
-
-        override fun getItemCount(): Int {
-            return items.size
-        }
-
-        @CallSuper
-        override fun getItemViewType(position: Int): Int {
-            val item = items[position]
-            return when (item) {
-                is Item.Header -> VIEW_TYPE_HEADER
-                is Item.Separator -> VIEW_TYPE_SEPARATOR
-                is Item.Radio -> VIEW_TYPE_RADIO
-                is Item.Checkbox -> VIEW_TYPE_CHECKBOX
-                is Item.MultiState -> VIEW_TYPE_MULTISTATE
-            }
-        }
-
-        @CallSuper
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
-            return when (viewType) {
-                VIEW_TYPE_HEADER -> HeaderHolder(parent)
-                VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
-                VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
-                VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
-                VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
-                else -> throw Exception("Unknown view type")
-            }
-        }
-
-        @CallSuper
-        override fun onBindViewHolder(holder: Holder, position: Int) {
-            when (holder) {
-                is HeaderHolder -> {
-                    val item = items[position] as Item.Header
-                    holder.title.setText(item.resTitle)
-                }
-                is SeparatorHolder -> {
-                    val view = holder.itemView
-                    val item = items[position] as Item.Separator
-                    view.setPadding(0, item.paddingTop, 0, item.paddingBottom)
-                }
-                is RadioHolder -> {
-                    val item = items[position] as Item.Radio
-                    holder.radio.setText(item.resTitle)
-                    holder.radio.isChecked = item.checked
-                }
-                is CheckboxHolder -> {
-                    val item = items[position] as Item.CheckboxGroup
-                    holder.check.setText(item.resTitle)
-                    holder.check.isChecked = item.checked
-                }
-                is MultiStateHolder -> {
-                    val item = items[position] as Item.MultiStateGroup
-                    val drawable = item.getStateDrawable(context)
-                    holder.text.setText(item.resTitle)
-                    holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
-                }
-            }
-        }
-
-        abstract fun onItemClicked(item: Item)
-
-    }
-
+package eu.kanade.tachiyomi.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.support.annotation.CallSuper
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.support.v4.content.ContextCompat
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.getResourceColor
+
+/**
+ * An alternative implementation of [android.support.design.widget.NavigationView], without menu
+ * inflation and allowing customizable items (multiple selections, custom views, etc).
+ */
+open class ExtendedNavigationView @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0)
+    : SimpleNavigationView(context, attrs, defStyleAttr) {
+
+    /**
+     * Every item of the nav view. Generic items must belong to this list, custom items could be
+     * implemented by an abstract class. If more customization is needed in the future, this can be
+     * changed to an interface instead of sealed class.
+     */
+    sealed class Item {
+        /**
+         * A view separator.
+         */
+        class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
+
+        /**
+         * A header with a title.
+         */
+        class Header(val resTitle: Int) : Item()
+
+        /**
+         * A checkbox.
+         */
+        open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item()
+
+        /**
+         * A checkbox belonging to a group. The group must handle selections and restrictions.
+         */
+        class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false)
+            : Checkbox(resTitle, checked), GroupedItem
+
+        /**
+         * A radio belonging to a group (a sole radio makes no sense). The group must handle
+         * selections and restrictions.
+         */
+        class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false)
+            : Item(), GroupedItem
+
+        /**
+         * An item with which needs more than two states (selected/deselected).
+         */
+        abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() {
+
+            /**
+             * Returns the drawable associated to every possible each state.
+             */
+            abstract fun getStateDrawable(context: Context): Drawable?
+
+            /**
+             * Creates a vector tinted with the accent color.
+             *
+             * @param context any context.
+             * @param resId the vector resource to load and tint
+             */
+            fun tintVector(context: Context, resId: Int): Drawable {
+                return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply {
+                    setTint(context.getResourceColor(R.attr.colorAccent))
+                }
+            }
+        }
+
+        /**
+         * An item with which needs more than two states (selected/deselected) belonging to a group.
+         * The group must handle selections and restrictions.
+         */
+        abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0)
+            : MultiState(resTitle, state), GroupedItem
+
+        /**
+         * A multistate item for sorting lists (unselected, ascending, descending).
+         */
+        class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
+
+            companion object {
+                const val SORT_NONE = 0
+                const val SORT_ASC = 1
+                const val SORT_DESC = 2
+            }
+
+            override fun getStateDrawable(context: Context): Drawable? {
+                return when (state) {
+                    SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp)
+                    SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp)
+                    SORT_NONE -> ContextCompat.getDrawable(context, R.drawable.empty_drawable_32dp)
+                    else -> null
+                }
+            }
+
+        }
+    }
+
+    /**
+     * Interface for an item belonging to a group.
+     */
+    interface GroupedItem {
+        val group: Group
+    }
+
+    /**
+     * A group containing a list of items.
+     */
+    interface Group {
+
+        /**
+         * An optional header for the group, typically a [Item.Header].
+         */
+        val header: Item?
+
+        /**
+         * An optional footer for the group, typically a [Item.Separator].
+         */
+        val footer: Item?
+
+        /**
+         * The items of the group, excluding header and footer.
+         */
+        val items: List<Item>
+
+        /**
+         * Creates all the elements of this group. Implementations can override this method for more
+         * customization.
+         */
+        fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
+
+        /**
+         * Called after creating the list of items. Implementations should load the current values
+         * into the models.
+         */
+        fun initModels()
+
+        /**
+         * Called when an item of this group is clicked. The group is responsible for all the
+         * selections of its items.
+         */
+        fun onItemClicked(item: Item)
+
+    }
+
+    /**
+     * Base adapter for the navigation view. It knows how to create and render every subclass of
+     * [Item].
+     */
+    abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
+
+        private val onClick = View.OnClickListener {
+            val pos = recycler.getChildAdapterPosition(it)
+            val item = items[pos]
+            onItemClicked(item)
+        }
+
+        fun notifyItemChanged(item: Item) {
+            val pos = items.indexOf(item)
+            if (pos != -1) notifyItemChanged(pos)
+        }
+
+        override fun getItemCount(): Int {
+            return items.size
+        }
+
+        @CallSuper
+        override fun getItemViewType(position: Int): Int {
+            val item = items[position]
+            return when (item) {
+                is Item.Header -> VIEW_TYPE_HEADER
+                is Item.Separator -> VIEW_TYPE_SEPARATOR
+                is Item.Radio -> VIEW_TYPE_RADIO
+                is Item.Checkbox -> VIEW_TYPE_CHECKBOX
+                is Item.MultiState -> VIEW_TYPE_MULTISTATE
+            }
+        }
+
+        @CallSuper
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
+            return when (viewType) {
+                VIEW_TYPE_HEADER -> HeaderHolder(parent)
+                VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
+                VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
+                VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
+                VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
+                else -> throw Exception("Unknown view type")
+            }
+        }
+
+        @CallSuper
+        override fun onBindViewHolder(holder: Holder, position: Int) {
+            when (holder) {
+                is HeaderHolder -> {
+                    val item = items[position] as Item.Header
+                    holder.title.setText(item.resTitle)
+                }
+                is SeparatorHolder -> {
+                    val view = holder.itemView
+                    val item = items[position] as Item.Separator
+                    view.setPadding(0, item.paddingTop, 0, item.paddingBottom)
+                }
+                is RadioHolder -> {
+                    val item = items[position] as Item.Radio
+                    holder.radio.setText(item.resTitle)
+                    holder.radio.isChecked = item.checked
+                }
+                is CheckboxHolder -> {
+                    val item = items[position] as Item.CheckboxGroup
+                    holder.check.setText(item.resTitle)
+                    holder.check.isChecked = item.checked
+                }
+                is MultiStateHolder -> {
+                    val item = items[position] as Item.MultiStateGroup
+                    val drawable = item.getStateDrawable(context)
+                    holder.text.setText(item.resTitle)
+                    holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
+                }
+            }
+        }
+
+        abstract fun onItemClicked(item: Item)
+
+    }
+
 }

+ 7 - 7
app/src/main/res/drawable/empty_drawable_32dp.xml

@@ -1,8 +1,8 @@
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/transparent"/>
-    <size
-        android:width="32dp"
-        android:height="32dp" />
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/transparent"/>
+    <size
+        android:width="32dp"
+        android:height="32dp" />
 </shape>

+ 9 - 9
app/src/main/res/drawable/ic_done_white_18dp.xml

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

+ 9 - 9
app/src/main/res/drawable/ic_watch_later_black_24dp.xml

@@ -1,9 +1,9 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13L11,7h1.5v5.2l4.5,2.7 -0.8,1.3z"/>
-</vector>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13L11,7h1.5v5.2l4.5,2.7 -0.8,1.3z"/>
+</vector>

+ 23 - 23
app/src/main/res/layout/navigation_view_checkbox.xml

@@ -1,23 +1,23 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
-    android:paddingRight="?attr/listPreferredItemPaddingRight"
-    android:background="?attr/selectableItemBackground"
-    android:focusable="true">
-
-    <CheckBox
-        android:id="@+id/nav_view_item"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
-        android:background="@android:color/transparent"
-        android:gravity="center_vertical|start"
-        android:maxLines="1"
-        android:clickable="false"
-        android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
-
-</LinearLayout>
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/listPreferredItemHeightSmall"
+    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
+    android:paddingRight="?attr/listPreferredItemPaddingRight"
+    android:background="?attr/selectableItemBackground"
+    android:focusable="true">
+
+    <CheckBox
+        android:id="@+id/nav_view_item"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
+        android:background="@android:color/transparent"
+        android:gravity="center_vertical|start"
+        android:maxLines="1"
+        android:clickable="false"
+        android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
+
+</LinearLayout>

+ 29 - 29
app/src/main/res/layout/navigation_view_group.xml

@@ -1,30 +1,30 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    android:background="?colorPrimary"
-    android:orientation="horizontal"
-    android:gravity="center_vertical"
-    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
-    android:paddingRight="?attr/listPreferredItemPaddingRight"
-    android:elevation="2dp">
-
-    <TextView
-        android:id="@+id/title"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
-        android:textColor="@color/textColorPrimaryDark"
-        tools:text="Header"/>
-
-    <ImageView
-        android:id="@+id/expand_icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/listPreferredItemHeightSmall"
+    android:background="?colorPrimary"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
+    android:paddingRight="?attr/listPreferredItemPaddingRight"
+    android:elevation="2dp">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+        android:textColor="@color/textColorPrimaryDark"
+        tools:text="Header"/>
+
+    <ImageView
+        android:id="@+id/expand_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
 </LinearLayout>

+ 61 - 61
app/src/main/res/layout/pref_item_source.xml

@@ -1,62 +1,62 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?selectableItemBackground"
-    android:baselineAligned="false"
-    android:clipToPadding="false"
-    android:gravity="center_vertical"
-    android:minHeight="42dp"
-    android:paddingLeft="?listPreferredItemPaddingLeft"
-    android:paddingRight="?listPreferredItemPaddingRight"
-    tools:ignore="RtlHardcoded">
-
-    <LinearLayout
-        android:id="@android:id/widget_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:clipToPadding="false"
-        android:gravity="start|center_vertical"
-        android:orientation="vertical"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp"/>
-
-    <TextView
-        android:id="@android:id/title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:ellipsize="marquee"
-        android:singleLine="true"
-        android:textAppearance="?textAppearanceListItem"/>
-
-    <!-- Hidden view -->
-    <TextView
-        android:id="@android:id/summary"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:visibility="gone" />
-
-    <LinearLayout
-        android:id="@+id/login_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_marginEnd="-16dp"
-        android:layout_marginRight="-16dp"
-        android:clipToPadding="false"
-        android:gravity="end|center_vertical"
-        android:orientation="vertical"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp"
-        android:visibility="gone">
-
-        <ImageView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:id="@+id/login" />
-
-    </LinearLayout>
-
-
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?selectableItemBackground"
+    android:baselineAligned="false"
+    android:clipToPadding="false"
+    android:gravity="center_vertical"
+    android:minHeight="42dp"
+    android:paddingLeft="?listPreferredItemPaddingLeft"
+    android:paddingRight="?listPreferredItemPaddingRight"
+    tools:ignore="RtlHardcoded">
+
+    <LinearLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:gravity="start|center_vertical"
+        android:orientation="vertical"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"/>
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:ellipsize="marquee"
+        android:singleLine="true"
+        android:textAppearance="?textAppearanceListItem"/>
+
+    <!-- Hidden view -->
+    <TextView
+        android:id="@android:id/summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone" />
+
+    <LinearLayout
+        android:id="@+id/login_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="-16dp"
+        android:layout_marginRight="-16dp"
+        android:clipToPadding="false"
+        android:gravity="end|center_vertical"
+        android:orientation="vertical"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:visibility="gone">
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/login" />
+
+    </LinearLayout>
+
+
 </LinearLayout>

+ 190 - 190
app/src/main/res/layout/track_item.xml

@@ -1,191 +1,191 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/track"
-    style="@style/Theme.Widget.CardView.Item"
-    android:padding="0dp">
-
-    <android.support.constraint.ConstraintLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <FrameLayout
-            android:id="@+id/logo_container"
-            android:layout_width="48dp"
-            android:layout_height="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            android:clickable="true"
-            tools:background="#2E51A2">
-
-            <ImageView
-                android:id="@+id/track_logo"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                tools:src="@drawable/mal" />
-
-        </FrameLayout>
-
-        <LinearLayout
-            android:id="@+id/title_container"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:background="?attr/selectable_list_drawable"
-            android:clickable="true"
-            android:padding="16dp"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toTopOf="parent">
-
-            <TextView
-                style="@style/TextAppearance.Regular.Body1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/title" />
-
-            <TextView
-                android:id="@+id/track_title"
-                style="@style/TextAppearance.Medium.Button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="4dp"
-                android:layout_marginStart="4dp"
-                android:ellipsize="middle"
-                android:gravity="end"
-                android:maxLines="1"
-                android:text="@string/action_edit" />
-
-        </LinearLayout>
-
-        <View
-            android:id="@+id/divider1"
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_marginEnd="16dp"
-            android:layout_marginLeft="16dp"
-            android:layout_marginRight="16dp"
-            android:layout_marginStart="16dp"
-            android:background="?android:attr/divider"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/title_container" />
-
-        <LinearLayout
-            android:id="@+id/status_container"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:background="?attr/selectable_list_drawable"
-            android:clickable="true"
-            android:padding="16dp"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/divider1">
-
-            <TextView
-                style="@style/TextAppearance.Regular.Body1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/status" />
-
-            <TextView
-                android:id="@+id/track_status"
-                style="@style/TextAppearance.Regular.Body1.Secondary"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="4dp"
-                android:layout_marginStart="4dp"
-                android:gravity="end"
-                tools:text="Reading" />
-
-        </LinearLayout>
-
-        <View
-            android:id="@+id/divider2"
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_marginEnd="16dp"
-            android:layout_marginLeft="16dp"
-            android:layout_marginRight="16dp"
-            android:layout_marginStart="16dp"
-            android:background="?android:attr/divider"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/status_container" />
-
-        <LinearLayout
-            android:id="@+id/chapters_container"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:background="?attr/selectable_list_drawable"
-            android:clickable="true"
-            android:padding="16dp"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/divider2">
-
-            <TextView
-                style="@style/TextAppearance.Regular.Body1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/chapters" />
-
-            <TextView
-                android:id="@+id/track_chapters"
-                style="@style/TextAppearance.Regular.Body1.Secondary"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="4dp"
-                android:layout_marginStart="4dp"
-                android:gravity="end"
-                tools:text="12/24" />
-
-        </LinearLayout>
-
-        <View
-            android:id="@+id/divider3"
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_marginEnd="16dp"
-            android:layout_marginLeft="16dp"
-            android:layout_marginRight="16dp"
-            android:layout_marginStart="16dp"
-            android:background="?android:attr/divider"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/chapters_container" />
-
-        <LinearLayout
-            android:id="@+id/score_container"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:background="?attr/selectable_list_drawable"
-            android:clickable="true"
-            android:padding="16dp"
-            app:layout_constraintLeft_toRightOf="@+id/logo_container"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/divider3">
-
-            <TextView
-                style="@style/TextAppearance.Regular.Body1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/score" />
-
-            <TextView
-                android:id="@+id/track_score"
-                style="@style/TextAppearance.Regular.Body1.Secondary"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="4dp"
-                android:layout_marginStart="4dp"
-                android:gravity="end"
-                tools:text="10" />
-
-        </LinearLayout>
-
-    </android.support.constraint.ConstraintLayout>
-
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/track"
+    style="@style/Theme.Widget.CardView.Item"
+    android:padding="0dp">
+
+    <android.support.constraint.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <FrameLayout
+            android:id="@+id/logo_container"
+            android:layout_width="48dp"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            android:clickable="true"
+            tools:background="#2E51A2">
+
+            <ImageView
+                android:id="@+id/track_logo"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                tools:src="@drawable/mal" />
+
+        </FrameLayout>
+
+        <LinearLayout
+            android:id="@+id/title_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:background="?attr/selectable_list_drawable"
+            android:clickable="true"
+            android:padding="16dp"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <TextView
+                style="@style/TextAppearance.Regular.Body1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/title" />
+
+            <TextView
+                android:id="@+id/track_title"
+                style="@style/TextAppearance.Medium.Button"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:ellipsize="middle"
+                android:gravity="end"
+                android:maxLines="1"
+                android:text="@string/action_edit" />
+
+        </LinearLayout>
+
+        <View
+            android:id="@+id/divider1"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_marginEnd="16dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginRight="16dp"
+            android:layout_marginStart="16dp"
+            android:background="?android:attr/divider"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/title_container" />
+
+        <LinearLayout
+            android:id="@+id/status_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:background="?attr/selectable_list_drawable"
+            android:clickable="true"
+            android:padding="16dp"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/divider1">
+
+            <TextView
+                style="@style/TextAppearance.Regular.Body1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/status" />
+
+            <TextView
+                android:id="@+id/track_status"
+                style="@style/TextAppearance.Regular.Body1.Secondary"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:gravity="end"
+                tools:text="Reading" />
+
+        </LinearLayout>
+
+        <View
+            android:id="@+id/divider2"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_marginEnd="16dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginRight="16dp"
+            android:layout_marginStart="16dp"
+            android:background="?android:attr/divider"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/status_container" />
+
+        <LinearLayout
+            android:id="@+id/chapters_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:background="?attr/selectable_list_drawable"
+            android:clickable="true"
+            android:padding="16dp"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/divider2">
+
+            <TextView
+                style="@style/TextAppearance.Regular.Body1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/chapters" />
+
+            <TextView
+                android:id="@+id/track_chapters"
+                style="@style/TextAppearance.Regular.Body1.Secondary"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:gravity="end"
+                tools:text="12/24" />
+
+        </LinearLayout>
+
+        <View
+            android:id="@+id/divider3"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_marginEnd="16dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginRight="16dp"
+            android:layout_marginStart="16dp"
+            android:background="?android:attr/divider"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/chapters_container" />
+
+        <LinearLayout
+            android:id="@+id/score_container"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:background="?attr/selectable_list_drawable"
+            android:clickable="true"
+            android:padding="16dp"
+            app:layout_constraintLeft_toRightOf="@+id/logo_container"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/divider3">
+
+            <TextView
+                style="@style/TextAppearance.Regular.Body1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/score" />
+
+            <TextView
+                android:id="@+id/track_score"
+                style="@style/TextAppearance.Regular.Body1.Secondary"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:gravity="end"
+                tools:text="10" />
+
+        </LinearLayout>
+
+    </android.support.constraint.ConstraintLayout>
+
 </android.support.v7.widget.CardView>