mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Unix line endings
This commit is contained in:
		@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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>>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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())
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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/"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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 = ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user