Unix line endings
This commit is contained in:
parent
cae04656b9
commit
c4dad1c20b
24
.gitattributes
vendored
Normal file
24
.gitattributes
vendored
Normal 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
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
|
|
||||||
}
|
}
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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>>
|
||||||
|
|
||||||
}
|
}
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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())
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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) })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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/"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user