Unix line endings

This commit is contained in:
arkon 2020-02-29 13:03:29 -05:00
parent cae04656b9
commit c4dad1c20b
96 changed files with 9761 additions and 9737 deletions

24
.gitattributes vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,367 +1,367 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.Exception import java.lang.Exception
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
/** /**
* A simple implementation for sources from a website. * A simple implementation for sources from a website.
*/ */
abstract class HttpSource : CatalogueSource { abstract class HttpSource : CatalogueSource {
/** /**
* Network service. * Network service.
*/ */
protected val network: NetworkHelper by injectLazy() protected val network: NetworkHelper by injectLazy()
// /** // /**
// * Preferences that a source may need. // * Preferences that a source may need.
// */ // */
// val preferences: SharedPreferences by lazy { // val preferences: SharedPreferences by lazy {
// Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE) // Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
// } // }
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
*/ */
abstract val baseUrl: String abstract val baseUrl: String
/** /**
* Version id used to generate the source id. If the site completely changes and urls are * 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. * incompatible, you may increase this value and it'll be considered as a new source.
*/ */
open val versionId = 1 open val versionId = 1
/** /**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits) * 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 * of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0. * Note the generated id sets the sign bit to 0.
*/ */
override val id by lazy { override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId" val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) 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 (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
} }
/** /**
* Headers used for requests. * Headers used for requests.
*/ */
val headers: Headers by lazy { headersBuilder().build() } val headers: Headers by lazy { headersBuilder().build() }
/** /**
* Default network client for doing requests. * Default network client for doing requests.
*/ */
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = network.client
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
open protected fun headersBuilder() = Headers.Builder().apply { open protected fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
} }
/** /**
* Visible name of the source. * Visible name of the source.
*/ */
override fun toString() = "$name (${lang.toUpperCase()})" override fun toString() = "$name (${lang.toUpperCase()})"
/** /**
* Returns an observable containing a page with a list of manga. Normally it's not needed to * Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method. * override this method.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
popularMangaParse(response) popularMangaParse(response)
} }
} }
/** /**
* Returns the request for the popular manga given the page. * Returns the request for the popular manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
abstract protected fun popularMangaRequest(page: Int): Request abstract protected fun popularMangaRequest(page: Int): Request
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun popularMangaParse(response: Response): MangasPage abstract protected fun popularMangaParse(response: Response): MangasPage
/** /**
* Returns an observable containing a page with a list of manga. Normally it's not needed to * Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method. * override this method.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
searchMangaParse(response) searchMangaParse(response)
} }
} }
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun searchMangaParse(response: Response): MangasPage abstract protected fun searchMangaParse(response: Response): MangasPage
/** /**
* Returns an observable containing a page with a list of latest manga updates. * Returns an observable containing a page with a list of latest manga updates.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
latestUpdatesParse(response) latestUpdatesParse(response)
} }
} }
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
abstract protected fun latestUpdatesRequest(page: Int): Request abstract protected fun latestUpdatesRequest(page: Int): Request
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun latestUpdatesParse(response: Response): MangasPage abstract protected fun latestUpdatesParse(response: Response): MangasPage
/** /**
* Returns an observable with the updated details for a manga. Normally it's not needed to * Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method. * override this method.
* *
* @param manga the manga to be updated. * @param manga the manga to be updated.
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mangaDetailsParse(response).apply { initialized = true } mangaDetailsParse(response).apply { initialized = true }
} }
} }
/** /**
* Returns the request for the details of a manga. Override only if it's needed to change the * 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. * url, send different headers or request method like POST.
* *
* @param manga the manga to be updated. * @param manga the manga to be updated.
*/ */
open fun mangaDetailsRequest(manga: SManga): Request { open fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return GET(baseUrl + manga.url, headers)
} }
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun mangaDetailsParse(response: Response): SManga abstract protected fun mangaDetailsParse(response: Response): SManga
/** /**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method. If a manga is licensed an empty chapter list observable is returned * override this method. If a manga is licensed an empty chapter list observable is returned
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
if (manga.status != SManga.LICENSED) { if (manga.status != SManga.LICENSED) {
return client.newCall(chapterListRequest(manga)) return client.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
chapterListParse(response) chapterListParse(response)
} }
} else { } else {
return Observable.error(Exception("Licensed - No chapters to show")) 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 * 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. * the url, send different headers or request method like POST.
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
open protected fun chapterListRequest(manga: SManga): Request { open protected fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return GET(baseUrl + manga.url, headers)
} }
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun chapterListParse(response: Response): List<SChapter> abstract protected fun chapterListParse(response: Response): List<SChapter>
/** /**
* Returns an observable with the page list for a chapter. * Returns an observable with the page list for a chapter.
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter whose page list has to be fetched.
*/ */
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
pageListParse(response) pageListParse(response)
} }
} }
/** /**
* Returns the request for getting the page list. Override only if it's needed to override the * Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST. * url, send different headers or request method like POST.
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter whose page list has to be fetched.
*/ */
open protected fun pageListRequest(chapter: SChapter): Request { open protected fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers) return GET(baseUrl + chapter.url, headers)
} }
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun pageListParse(response: Response): List<Page> abstract protected fun pageListParse(response: Response): List<Page>
/** /**
* Returns an observable with the page containing the source url of the image. If there's any * Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception. * error, it will return null instead of throwing an exception.
* *
* @param page the page whose source image has to be fetched. * @param page the page whose source image has to be fetched.
*/ */
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { imageUrlParse(it) } .map { imageUrlParse(it) }
} }
/** /**
* Returns the request for getting the url to the source image. Override only if it's needed to * 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. * override the url, send different headers or request method like POST.
* *
* @param page the chapter whose page list has to be fetched * @param page the chapter whose page list has to be fetched
*/ */
open protected fun imageUrlRequest(page: Page): Request { open protected fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers) return GET(page.url, headers)
} }
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
abstract protected fun imageUrlParse(response: Response): String abstract protected fun imageUrlParse(response: Response): String
/** /**
* Returns an observable with the response of the source image. * Returns an observable with the response of the source image.
* *
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
fun fetchImage(page: Page): Observable<Response> { fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page) return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess() .asObservableSuccess()
} }
/** /**
* Returns the request for getting the source image. Override only if it's needed to override * Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST. * the url, send different headers or request method like POST.
* *
* @param page the chapter whose page list has to be fetched * @param page the chapter whose page list has to be fetched
*/ */
open protected fun imageRequest(page: Page): Request { open protected fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers) return GET(page.imageUrl!!, headers)
} }
/** /**
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from * 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. * database and the urls could still work after a domain change.
* *
* @param url the full url to the chapter. * @param url the full url to the chapter.
*/ */
fun SChapter.setUrlWithoutDomain(url: String) { fun SChapter.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url) this.url = getUrlWithoutDomain(url)
} }
/** /**
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from * 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. * database and the urls could still work after a domain change.
* *
* @param url the full url to the manga. * @param url the full url to the manga.
*/ */
fun SManga.setUrlWithoutDomain(url: String) { fun SManga.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url) this.url = getUrlWithoutDomain(url)
} }
/** /**
* Returns the url of the given string without the scheme and domain. * Returns the url of the given string without the scheme and domain.
* *
* @param orig the full url. * @param orig the full url.
*/ */
private fun getUrlWithoutDomain(orig: String): String { private fun getUrlWithoutDomain(orig: String): String {
try { try {
val uri = URI(orig) val uri = URI(orig)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null)
out += "?" + uri.query out += "?" + uri.query
if (uri.fragment != null) if (uri.fragment != null)
out += "#" + uri.fragment out += "#" + uri.fragment
return out return out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
return orig return orig
} }
} }
/** /**
* Called before inserting a new chapter into database. Use it if you need to override chapter * Called before inserting a new chapter into database. Use it if you need to override chapter
* fields, like the title or the chapter number. Do not change anything to [manga]. * fields, like the title or the chapter number. Do not change anything to [manga].
* *
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
open fun prepareNewChapter(chapter: SChapter, manga: SManga) { open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
} }
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.
*/ */
override fun getFilterList() = FilterList() override fun getFilterList() = FilterList()
} }

View File

@ -1,25 +1,25 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import rx.Observable import rx.Observable
fun HttpSource.getImageUrl(page: Page): Observable<Page> { fun HttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE page.status = Page.LOAD_PAGE
return fetchImageUrl(page) return fetchImageUrl(page)
.doOnError { page.status = Page.ERROR } .doOnError { page.status = Page.ERROR }
.onErrorReturn { null } .onErrorReturn { null }
.doOnNext { page.imageUrl = it } .doOnNext { page.imageUrl = it }
.map { page } .map { page }
} }
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages) return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() } .filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages)) .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
} }
fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages) return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() } .filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) } .concatMap { getImageUrl(it) }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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