androidx migration

I DID THIS ONE MYSELF WITHOUT TAKING IT FROM THE OTHER FORKS
YEEEEEEEEEEET
This commit is contained in:
Rani Sargees 2020-01-06 03:26:31 -05:00
parent 53402459f2
commit 9b883b1a09
243 changed files with 4537 additions and 4604 deletions

View File

@ -40,7 +40,7 @@ android {
applicationId "eu.kanade.tachiyomi.az"
minSdkVersion 16
targetSdkVersion 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 8405
versionName "v8.4.5-AZ"
@ -134,22 +134,21 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
final support_library_version = '28.0.0'
implementation "com.android.support:support-v4:$support_library_version"
implementation "com.android.support:appcompat-v7:$support_library_version"
implementation "com.android.support:cardview-v7:$support_library_version"
implementation "com.android.support:design:$support_library_version"
implementation "com.android.support:recyclerview-v7:$support_library_version"
implementation "com.android.support:preference-v7:$support_library_version"
implementation "com.android.support:support-annotations:$support_library_version"
implementation "com.android.support:customtabs:$support_library_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.android.support:multidex:1.0.3'
implementation 'androidx.multidex:multidex:2.0.1'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
standardImplementation 'com.google.firebase:firebase-core:16.0.9'
standardImplementation 'com.google.firebase:firebase-core:17.2.1'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@ -159,11 +158,11 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client
implementation "com.squareup.okhttp3:okhttp:3.12.3" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
implementation 'com.squareup.okio:okio:1.17.4' // TODO I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x?
implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
implementation 'com.squareup.okio:okio:2.4.0' // I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x?
// REST
final retrofit_version = '2.6.1'
final retrofit_version = '2.6.2'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
@ -185,16 +184,16 @@ dependencies {
// Job scheduling
implementation 'com.evernote:android-job:1.2.5'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
implementation 'com.google.android.gms:play-services-gcm:16.1.0'
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
// [EXH] Android 7 SSL Workaround
implementation 'com.google.android.gms:play-services-safetynet:16.0.0'
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
// Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
implementation 'android.arch.persistence:db:1.1.1'
implementation 'androidx.sqlite:sqlite:2.0.1'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.25.2'
@ -208,13 +207,13 @@ dependencies {
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
// Image library
final glide_version = '4.9.0'
final glide_version = '4.10.0'
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Transformations
implementation 'jp.wasabeef:glide-transformations:3.1.1'
implementation 'jp.wasabeef:glide-transformations:4.0.0'
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
@ -225,24 +224,24 @@ dependencies {
// UI
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
implementation 'eu.davidea:flexible-adapter:5.0.6' // Cannot upgrade to 5.1.0 as it uses AndroidX
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b5'
implementation 'eu.davidea:flexible-adapter:5.1.0' // Cannot upgrade to 5.1.0 as it uses AndroidX
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.github.amulyakhare:TextDrawable:558677e'
implementation 'com.afollestad.material-dialogs:core:0.9.6.0' // Cannot upgrade to 2.x, AndroidX and API changes
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
implementation 'com.github.mthli:Slice:v1.3'
implementation 'me.gujun.android.taggroup:library:1.4@aar'
implementation 'com.github.chrisbanes:PhotoView:2.1.4' // Cannot upgrade to 2.2.x+ as it uses AndroidX
implementation 'com.github.inorichi:DirectionalViewPager:3acc51a'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
// Conductor
implementation 'com.bluelinelabs:conductor:2.1.5'
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude group: "com.android.support"
}
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
implementation 'com.github.inorichi:conductor-support-preference:78e2344'
// RxBindings
final rxbindings_version = '1.0.1'
@ -287,7 +286,7 @@ dependencies {
implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1'
// Debug network interceptor (EH)
implementation "com.squareup.okhttp3:logging-interceptor:3.12.1"
implementation "com.squareup.okhttp3:logging-interceptor:4.2.1"
// Firebase (EH)
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
@ -309,7 +308,7 @@ dependencies {
// Humanize (EH)
implementation 'com.github.mfornos:humanize-slim:1.2.2'
implementation 'com.android.support:gridlayout-v7:28.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
final def markwon_version = '4.1.0'
@ -319,6 +318,8 @@ dependencies {
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image:$markwon_version"
implementation "io.noties.markwon:linkify:$markwon_version"
implementation 'com.google.guava:guava:27.0.1-android'
}
buildscript {

View File

@ -111,7 +111,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">

View File

@ -6,7 +6,7 @@ import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Environment
import android.support.multidex.MultiDex
import androidx.multidex.MultiDex
import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel
import com.elvishew.xlog.XLog

View File

@ -12,10 +12,9 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.saveTo
import okhttp3.Response
import okio.Okio
import okio.buffer
import okio.sink
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@ -147,7 +146,7 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: return
// Write chapter urls to cache.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
editor.newOutputStream(0).sink().buffer().use {
it.write(cachedValue.toByteArray())
it.flush()
}
@ -207,12 +206,12 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
response.body()!!.source().saveTo(editor.newOutputStream(0))
response.body!!.source().saveTo(editor.newOutputStream(0))
diskCache.flush()
editor.commit()
} finally {
response.body()?.close()
response.body?.close()
editor?.abortUnlessCommitted()
}
}

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database
import android.arch.persistence.db.SupportSQLiteOpenHelper
import android.content.Context
import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.*
import eu.kanade.tachiyomi.data.database.models.*

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database
import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import android.support.annotation.NonNull
import androidx.annotation.NonNull
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.graphics.BitmapFactory
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue

View File

@ -9,7 +9,7 @@ import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay

View File

@ -362,7 +362,7 @@ class Downloader(
.map { response ->
val file = tmpDir.createFile("$filename.tmp")
try {
response.body()!!.source().saveTo(file.openOutputStream())
response.body!!.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file)
file.renameTo("$filename.$extension")
} catch (e: Exception) {
@ -394,7 +394,7 @@ class Downloader(
*/
private fun getImageExtension(response: Response, file: UniFile): String {
// Read content type if available.
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.

View File

@ -6,7 +6,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.notification.Notifications

View File

@ -6,7 +6,7 @@ import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -25,7 +25,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.isServiceRunning
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import rx.Observable
import rx.Subscription

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.preference
import android.support.v7.preference.PreferenceDataStore
import androidx.preference.PreferenceDataStore
class EmptyPreferenceDataStore : PreferenceDataStore() {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences
import android.support.v7.preference.PreferenceDataStore
import androidx.preference.PreferenceDataStore
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {

View File

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

View File

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

View File

@ -84,7 +84,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
@ -127,7 +127,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.asObservableSuccess()
.map { netResponse ->
// get comic info
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
jsonToTrack(parser.parse(responseBody).obj)
}
}
@ -144,7 +144,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return authClient.newCall(requestUserRead)
.asObservableSuccess()
.map { netResponse ->
val resp = netResponse.body()?.string()
val resp = netResponse.body?.string()
val coll = gson.fromJson(resp, Collection::class.java)
track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!!
@ -154,7 +154,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}

View File

@ -14,7 +14,7 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size()) {
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", tocken)
@ -29,18 +29,18 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
} else {
response.close()
}
}
var authRequest = if (originalRequest.method() == "GET") originalRequest.newBuilder()
var authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder()
.header("User-Agent", "Tachiyomi")
.url(originalRequest.url().newBuilder()
.url(originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.access_token).build())
.build() else originalRequest.newBuilder()
.post(addTocken(currAuth.access_token, originalRequest.body() as FormBody))
.post(addTocken(currAuth.access_token, originalRequest.body as FormBody))
.header("User-Agent", "Tachiyomi")
.build()

View File

@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) {
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
} else {
response.close()
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.kitsu
import android.support.annotation.CallSuper
import androidx.annotation.CallSuper
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Track

View File

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

View File

@ -15,7 +15,7 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
val request = chain.request()
var response = chain.proceed(updateRequest(request))
if (response.code() == 400){
if (response.code == 400) {
myanimelist.refreshLogin()
response = chain.proceed(updateRequest(request))
}
@ -24,7 +24,7 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
}
private fun updateRequest(request: Request): Request {
return request.body()?.let {
return request.body?.let {
val contentType = it.contentType().toString()
val updatedBody = when {
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)

View File

@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.Response
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
@ -85,7 +89,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.map {response ->
var libTrack: Track? = null
response.use {
if (it.priorResponse()?.isRedirect != true) {
if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
@ -125,7 +129,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
response.use {
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
}
}
@ -172,15 +176,15 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private fun Response.consumeBody(): String? {
use {
if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
return it.body()?.string()
if (it.code != 200) throw Exception("HTTP error ${it.code}")
return it.body?.string()
}
}
private fun Response.consumeXmlBody(): String? {
use { res ->
if (res.code() != 200) throw Exception("Export list error")
BufferedReader(InputStreamReader(GZIPInputStream(res.body()?.source()?.inputStream()))).use { reader ->
if (res.code != 200) throw Exception("Export list error")
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
val sb = StringBuilder()
reader.forEachLine { line ->
sb.append(line)
@ -262,7 +266,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.put("score", track.score)
.put("num_read_chapters", track.last_chapter_read)
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
return RequestBody.create("application/json; charset=utf-8".toMediaTypeOrNull(), body.toString())
}
private fun Element.searchTitle() = select("strong").text()!!

View File

@ -14,7 +14,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import okhttp3.*
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
@ -22,7 +26,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
private val gson: Gson by injectLazy()
private val parser = JsonParser()
private val jsonime = MediaType.parse("application/json; charset=utf-8")
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track, user_id: String): Observable<Track> {
@ -63,7 +67,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
@ -120,13 +124,13 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
return authClient.newCall(requestMangas)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
parser.parse(responseBody).obj
}.flatMap { mangas ->
authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
@ -143,13 +147,13 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
}
fun getCurrentUser(): Int {
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body()?.string()
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
return parser.parse(user).obj["id"].asInt
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}

View File

@ -22,7 +22,7 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept
if (currAuth.isExpired()) {
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
} else {
response.close()
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Intent
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.net.Uri
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver

View File

@ -71,7 +71,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
val apkFile = File(externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body()!!.source().saveTo(apkFile)
response.body!!.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")

View File

@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
}
private fun parseResponse(response: Response): List<Extension.Available> {
val text = response.body()?.use { it.string() } ?: return emptyList()
val text = response.body?.use { it.string() } ?: return emptyList()
val json = gson.fromJson<JsonArray>(text)

View File

@ -21,7 +21,7 @@ class AndroidCookieJar(context: Context) : CookieJar {
}
}
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString()
for (cookie in cookies) {

View File

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

View File

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

View File

@ -1,81 +1,81 @@
package eu.kanade.tachiyomi.network
import exh.util.withRootCause
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.util.concurrent.atomic.AtomicBoolean
fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>> {
// Record stacktrace at creation time for easier debugging
// asObservable is involved in a lot of crashes so this is worth the performance hit
val asyncStackTrace = Exception("Async stacktrace")
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
val executed = AtomicBoolean(false)
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
executed.set(true)
if (!subscriber.isUnsubscribed) {
subscriber.onNext(asyncStackTrace to response)
subscriber.onCompleted()
}
} catch (error: Throwable) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error.withRootCause(asyncStackTrace))
}
}
}
override fun unsubscribe() {
if(!executed.get())
call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}
fun Call.asObservable() = asObservableWithAsyncStacktrace().map { it.second }
fun Call.asObservableSuccess(): Observable<Response> {
return asObservableWithAsyncStacktrace().map { (asyncStacktrace, response) ->
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code()}", asyncStacktrace)
} else response
}
}
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body()!!, listener))
.build()
}
.build()
return progressClient.newCall(request)
package eu.kanade.tachiyomi.network
import exh.util.withRootCause
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.util.concurrent.atomic.AtomicBoolean
fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>> {
// Record stacktrace at creation time for easier debugging
// asObservable is involved in a lot of crashes so this is worth the performance hit
val asyncStackTrace = Exception("Async stacktrace")
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
val executed = AtomicBoolean(false)
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
executed.set(true)
if (!subscriber.isUnsubscribed) {
subscriber.onNext(asyncStackTrace to response)
subscriber.onCompleted()
}
} catch (error: Throwable) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error.withRootCause(asyncStackTrace))
}
}
}
override fun unsubscribe() {
if(!executed.get())
call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled()
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}
fun Call.asObservable() = asObservableWithAsyncStacktrace().map { it.second }
fun Call.asObservableSuccess(): Observable<Response> {
return asObservableWithAsyncStacktrace().map { (asyncStacktrace, response) ->
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}", asyncStacktrace)
} else response
}
}
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body!!, listener))
.build()
}
.build()
return progressClient.newCall(request)
}

View File

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

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.source
import android.support.v7.preference.PreferenceScreen
import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source {

View File

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

View File

@ -18,35 +18,36 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.metadata.nullIfBlank
import exh.metadata.parseHumanReadableByteCount
import exh.debug.DebugToggles
import exh.ui.login.LoginController
import exh.util.UriFilter
import exh.util.UriGroup
import exh.util.ignore
import exh.util.urlImportFetchSearchManga
import kotlinx.coroutines.runBlocking
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import rx.Observable
import rx.Single
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.util.*
import exh.metadata.metadata.base.RaisedTag
import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry
import kotlinx.coroutines.runBlocking
import org.jsoup.nodes.TextNode
import rx.Single
import java.lang.RuntimeException
// TODO Consider gallery updating when doing tabbed browsing
class EHentai(override val id: Long,
@ -108,11 +109,11 @@ class EHentai(override val id: Long,
})
}
val parsedLocation = HttpUrl.parse(doc.location())
val parsedLocation = doc.location().toHttpUrlOrNull()
//Add to page if required
val hasNextPage = if(parsedLocation == null
|| !parsedLocation.queryParameterNames().contains(REVERSE_PARAM)) {
|| !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
select("a[onclick=return false]").last()?.let {
it.text() == ">"
} ?: false
@ -151,7 +152,7 @@ class EHentai(override val id: Long,
throttleFunc()
val resp = client.newCall(exGet(baseUrl + url)).execute()
if (!resp.isSuccessful) error("HTTP error (${resp.code()})!")
if (!resp.isSuccessful) error("HTTP error (${resp.code})!")
doc = resp.asJsoup()
val parentLink = doc!!.select("#gdd .gdt1").find { el ->
@ -344,10 +345,10 @@ class EHentai(override val id: Long,
} else {
response.close()
if(response.code() == 404) {
if (response.code == 404) {
throw GalleryNotFoundException(stacktrace)
} else {
throw Exception("HTTP error ${response.code()}", stacktrace)
throw Exception("HTTP error ${response.code}", stacktrace)
}
}
}
@ -707,7 +708,7 @@ class EHentai(override val id: Long,
val outJson = JsonParser().parse(client.newCall(Request.Builder()
.url(EH_API_BASE)
.post(RequestBody.create(JSON, json.toString()))
.build()).execute().body()!!.string()).obj
.build()).execute().body!!.string()).obj
val obj = outJson["tokenlist"].array.first()
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
@ -720,7 +721,7 @@ class EHentai(override val id: Long,
private const val REVERSE_PARAM = "TEH_REVERSE"
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
private val JSON = MediaType.parse("application/json; charset=utf-8")!!
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
private val FAVORITES_BORDER_HEX_COLORS = listOf(
"000",

View File

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.GalleryAddEvent
import exh.HITOMI_SOURCE_ID
import exh.hitomi.HitomiNozomi
import exh.metadata.metadata.HitomiSearchMetadata
@ -265,7 +264,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
val range = response.header("Content-Range")!!
val total = range.substringAfter('/').toLong()
val end = range.substringBefore('/').substringAfter('-').toLong()
val body = response.body()!!
val body = response.body!!
return parseNozomiPage(body.bytes())
.map {
MangasPage(it, end < total - 1)
@ -360,8 +359,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
val hlId = response.request().url().pathSegments().last().removeSuffix(".js").toLong()
val str = response.body()!!.string()
val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
val str = response.body!!.string()
val json = jsonParser.parse(str.removePrefix("var galleryinfo ="))
return json.array.mapIndexed { index, jsonElement ->
Page(
@ -385,7 +384,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
override fun imageRequest(page: Page): Request {
val request = super.imageRequest(page)
val hlId = request.url().pathSegments().let {
val hlId = request.url.pathSegments.let {
it[it.lastIndex - 1]
}
return request.newBuilder()

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
@ -15,12 +13,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.GalleryAddEvent
import exh.NHENTAI_SOURCE_ID
import exh.metadata.metadata.NHentaiSearchMetadata
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedTag
import exh.util.*
import exh.util.urlImportFetchSearchManga
import okhttp3.Request
import okhttp3.Response
import rx.Observable
@ -153,17 +150,17 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
}
}
val hasNextPage = if(!response.request().url().queryParameterNames().contains(REVERSE_PARAM)) {
val hasNextPage = if (!response.request.url.queryParameterNames.contains(REVERSE_PARAM)) {
doc.selectFirst(".next") != null
} else {
response.request().url().queryParameter(REVERSE_PARAM)!!.toBoolean()
response.request.url.queryParameter(REVERSE_PARAM)!!.toBoolean()
}
return MangasPage(mangas, hasNextPage)
}
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val json = GALLERY_JSON_REGEX.find(input.body()!!.string())!!.groupValues[1]
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1]
val obj = jsonParser.parse(json).asJsonObject
with(metadata) {

View File

@ -17,9 +17,13 @@ import exh.util.CachedField
import exh.util.NakedTrie
import exh.util.await
import exh.util.urlImportFetchSearchManga
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@ -64,7 +68,7 @@ class EightMuses: HttpSource(),
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
.body()!!.string()
.body!!.string()
val parsed = Jsoup.parse(result)
@ -115,11 +119,11 @@ class EightMuses: HttpSource(),
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = if(!query.isBlank()) {
HttpUrl.parse("$baseUrl/search")!!
"$baseUrl/search".toHttpUrlOrNull()!!
.newBuilder()
.addQueryParameter("q", query)
} else {
HttpUrl.parse("$baseUrl/comics")!!
"$baseUrl/comics".toHttpUrlOrNull()!!
.newBuilder()
}

View File

@ -28,7 +28,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import okhttp3.*
import okhttp3.CookieJar
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
@ -371,7 +374,7 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
*/
override fun pageListParse(response: Response): List<Page> {
val doc = response.asJsoup()
val basePath = listOf("data") + response.request().url().pathSegments()
val basePath = listOf("data") + response.request.url.pathSegments
val scripts = doc.getElementsByTag("script").map { it.data() }
for(script in scripts) {
val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull() ?: continue

View File

@ -15,7 +15,7 @@ import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUA
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document
import rx.Observable
@ -72,7 +72,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
}
readerId = HttpUrl.parse(input.select("[title=Read]").attr("href"))!!.pathSegments()[2]
readerId = input.select("[title=Read]").attr("href").toHttpUrlOrNull()!!.pathSegments[2]
}
}

View File

@ -16,25 +16,23 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.toast
import exh.GalleryAddEvent
import exh.TSUMINO_SOURCE_ID
import exh.ui.captcha.ActionCompletionVerifier
import exh.ui.captcha.BrowserActionActivity
import exh.metadata.metadata.TsuminoSearchMetadata
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.ui.captcha.ActionCompletionVerifier
import exh.ui.captcha.BrowserActionActivity
import exh.util.urlImportFetchSearchManga
import okhttp3.*
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.io.IOException
class Tsumino(private val context: Context): ParsedHttpSource(),
LewdSource<TsuminoSearchMetadata, Document>,
@ -127,7 +125,7 @@ class Tsumino(private val context: Context): ParsedHttpSource(),
}
fun genericMangaParse(response: Response): MangasPage {
val json = jsonParser.parse(response.body()!!.string()!!).asJsonObject
val json = jsonParser.parse(response.body!!.string()!!).asJsonObject
val hasNextPage = json["pageNumber"].int < json["pageCount"].int
val manga = json["data"].array.map {

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.util.LocaleHelper
abstract class BaseActivity : AppCompatActivity() {

View File

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.base.controller
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.support.v4.content.ContextCompat
import androidx.core.content.ContextCompat
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.base.controller;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -14,6 +12,9 @@ import com.bluelinelabs.conductor.Router;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A controller that displays a dialog window, floating on top of its activity's window.
* This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle
import android.support.annotation.CallSuper
import android.view.View
import androidx.annotation.CallSuper
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription

View File

@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.support.v4.widget.DrawerLayout
import android.view.ViewGroup
interface SecondaryDrawerController {
fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup?
fun cleanupSecondaryDrawer(drawer: DrawerLayout)
fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.support.design.widget.TabLayout
import com.google.android.material.tabs.TabLayout
interface TabbedController {

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
abstract class BaseViewHolder(view: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView

View File

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

View File

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

View File

@ -3,9 +3,8 @@ package eu.kanade.tachiyomi.ui.catalogue
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Bundle
import android.os.Parcelable
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import androidx.appcompat.widget.SearchView
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
@ -111,7 +110,7 @@ class CatalogueController(bundle: Bundle? = null) : NucleusController<CatalogueP
adapter = CatalogueAdapter(this)
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@ -24,14 +23,14 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LangHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): LangHolder {
return LangHolder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: LangHolder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: LangHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)

View File

@ -4,10 +4,9 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
class SourceDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() {
private val divider: Drawable
@ -17,14 +16,14 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
override fun onDraw(c: Canvas, parent: androidx.recyclerview.widget.RecyclerView, state: androidx.recyclerview.widget.RecyclerView.State) {
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child)
if (holder is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val params = child.layoutParams as androidx.recyclerview.widget.RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
val left = parent.paddingLeft + holder.margin
@ -36,8 +35,8 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView,
state: androidx.recyclerview.widget.RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
@ -27,14 +26,14 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null,
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): SourceHolder {
return SourceHolder(view, adapter as CatalogueAdapter, showButtons)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: SourceHolder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: SourceHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)

View File

@ -2,13 +2,15 @@ package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.widget.*
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog
import com.elvishew.xlog.XLog
import com.f2prateek.rx.preferences.Preference
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@ -35,7 +37,6 @@ import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
@ -85,7 +86,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
private var recycler: androidx.recyclerview.widget.RecyclerView? = null
/**
* Subscription for the search view.
@ -142,13 +143,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? {
// Inflate and prepare drawer
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView //TODO whatever this is
this.navView = navView
navView.setFilters(presenter.filterItems)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
// EXH -->
navView.setSavedSearches(presenter.loadSearches())
@ -196,7 +197,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
showProgressBar()
adapter?.clear()
drawer.closeDrawer(Gravity.END)
drawer.closeDrawer(GravityCompat.END)
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
activity?.invalidateOptionsMenu()
}
@ -238,7 +239,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
drawer.closeDrawer(Gravity.END)
drawer.closeDrawer(GravityCompat.END)
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
}
@ -248,31 +249,31 @@ open class BrowseCatalogueController(bundle: Bundle) :
presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems)
}
return navView
return navView as ViewGroup //TODO fix this bullshit
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe()
var oldPosition = RecyclerView.NO_POSITION
var oldPosition = androidx.recyclerview.widget.RecyclerView.NO_POSITION
val oldRecycler = catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
if (oldRecycler is androidx.recyclerview.widget.RecyclerView) {
oldPosition = (oldRecycler.layoutManager as androidx.recyclerview.widget.LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null
catalogue_view?.removeView(oldRecycler)
}
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
androidx.recyclerview.widget.RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
layoutParams = androidx.recyclerview.widget.RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(context, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL))
}
} else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
@ -282,7 +283,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@BrowseCatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
(layoutManager as androidx.recyclerview.widget.GridLayoutManager).spanSizeLookup = object : androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) {
R.layout.catalogue_grid_item, null -> 1
@ -297,7 +298,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) {
if (oldPosition != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
recycler.layoutManager?.scrollToPosition(oldPosition)
}
this.recycler = recycler
@ -369,7 +370,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
R.id.action_open_in_browser -> openInBrowser()
R.id.action_open_in_web_view -> openInWebView()
else -> return super.onOptionsItemSelected(item)

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.support.v7.widget.RecyclerView
import android.view.Gravity
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@ -25,7 +24,7 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Bo
R.layout.catalogue_grid_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CatalogueHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CatalogueHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
view.apply {
@ -40,7 +39,7 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Bo
}
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
holder: CatalogueHolder,
position: Int,
payloads: List<Any?>?) {

View File

@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
@ -10,13 +12,12 @@ import android.widget.LinearLayout
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.SimpleNavigationView
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
import android.util.TypedValue
import android.view.View
import exh.EXHSavedSearch
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: SimpleNavigationView(context, attrs) {
@ -44,10 +45,10 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
init {
recycler.adapter = adapter
recycler.setHasFixedSize(true)
val view = inflate(eu.kanade.tachiyomi.R.layout.catalogue_drawer_content)
val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view)
title.text = context?.getString(eu.kanade.tachiyomi.R.string.source_search_options)
title.text = context.getString(R.string.source_search_options)
save_search_btn.setOnClickListener { onSaveClicked() }
search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.browse
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
@ -19,11 +18,11 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
return R.layout.catalogue_progress_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
holder.progressBar.visibility = View.GONE
holder.progressMessage.visibility = View.GONE

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.CheckBox
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -16,11 +15,11 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
return R.layout.navigation_view_checkbox
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.check
view.text = filter.name
view.isChecked = filter.state

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
@ -33,11 +32,11 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
return 101
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.title.text = filter.name
holder.icon.setVectorCompat(if (isExpanded)

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint
import android.support.design.R
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.TextView
import com.google.android.material.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -18,11 +17,11 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
return R.layout.design_navigation_item_subheader
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.itemView as TextView
view.text = filter.name
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.Button
import android.widget.TextView
@ -23,11 +22,11 @@ class HelpDialogItem(val filter: Filter.HelpDialog) : AbstractHeaderItem<HelpDia
return R.layout.navigation_view_help_dialog
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.button as TextView
view.text = filter.name
view.setOnClickListener {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Spinner
@ -19,11 +18,11 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
return R.layout.navigation_view_spinner
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.text.text = filter.name + ": "
val spinner = holder.spinner

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint
import android.support.design.R
import android.support.v7.widget.RecyclerView
import android.view.View
import com.google.android.material.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -17,11 +16,11 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
return R.layout.design_navigation_item_separator
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
}

View File

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

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.CheckedTextView
import androidx.core.content.ContextCompat
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -23,11 +22,11 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
return 102
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.text
view.text = name
val filter = group.filter

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.widget.TextInputLayout
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.EditText
import com.google.android.material.textfield.TextInputLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -23,17 +22,17 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
return R.layout.navigation_view_text
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.wrapper.hint = filter.name
holder.edit.setText(filter.state)
holder.edit.addTextChangedListener(textWatcher)
}
override fun unbindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int) {
override fun unbindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int) {
holder.edit.removeTextChangedListener(textWatcher)
}

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.R
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.CheckedTextView
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.google.android.material.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -24,11 +23,11 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
return 103
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.text
view.text = filter.name

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.os.Parcelable
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.source.CatalogueSource
@ -25,12 +24,12 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
*/
private var bundle = Bundle()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
override fun onViewRecycled(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
saveHolderState(holder, bundle)
}
@ -53,7 +52,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
* @param holder The holder to save.
* @param outState The bundle where the state is saved.
*/
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
private fun saveHolderState(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, outState: Bundle) {
val key = "holder_${holder.adapterPosition}"
val holderState = SparseArray<Parcelable>()
holder.itemView.saveHierarchyState(holderState)
@ -65,7 +64,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
*
* @param holder The holder to restore.
*/
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
private fun restoreHolderState(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder) {
val key = "holder_${holder.adapterPosition}"
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
if (holderState != null) {

View File

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

View File

@ -1,9 +1,8 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import androidx.appcompat.widget.SearchView
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
@ -139,7 +138,7 @@ open class CatalogueSearchController(
adapter = CatalogueSearchAdapter(this)
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
recycler.adapter = adapter
}

View File

@ -1,16 +1,12 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import eu.kanade.tachiyomi.R
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
/**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards.
@ -30,7 +26,7 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
init {
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
more.setOnClickListener {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -32,14 +31,14 @@ class CatalogueSearchItem(val source: CatalogueSource, val results: List<Catalog
*
* @return holder of view.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CatalogueSearchHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CatalogueSearchHolder {
return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: CatalogueSearchHolder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: CatalogueSearchHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.latest
import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
@ -28,11 +27,11 @@ class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle
menu.findItem(R.id.action_set_filter).isVisible = false
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? {
return null
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
}

View File

@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.category
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding.view.clicks
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
@ -74,7 +73,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
super.onViewCreated(view)
adapter = CategoryAdapter(this@CategoryController)
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
@ -207,7 +206,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*/
override fun onItemClick(view: View, position: Int): Boolean {
// Check if action mode is initialized and selected item exist.
if (actionMode != null && position != RecyclerView.NO_POSITION) {
if (actionMode != null && position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
toggleSelection(position)
return true
} else {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.category
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -31,7 +30,7 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CategoryHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CategoryHolder {
return CategoryHolder(view, adapter as CategoryAdapter)
}
@ -43,7 +42,7 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
holder: CategoryHolder,
position: Int,
payloads: List<Any?>?) {

View File

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

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import android.support.v7.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -28,7 +27,7 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem<DownloadHolder
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView
.ViewHolder>>): DownloadHolder {
return DownloadHolder(view, adapter as DownloadAdapter)
}
@ -41,8 +40,8 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem<DownloadHolder
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: DownloadHolder, position: Int, payloads: MutableList<Any>) {
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
holder: DownloadHolder, position: Int, payloads: MutableList<Any>) {
holder.bind(download)
}

View File

@ -1,12 +1,7 @@
package eu.kanade.tachiyomi.ui.extension
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.appcompat.widget.SearchView
import com.jakewharton.rxbinding.support.v4.widget.refreshes
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -63,7 +58,7 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
// Initialize adapter, scroll listener and recycler views
adapter = ExtensionAdapter(this)
// Create recycler and set adapter.
ext_recycler.layoutManager = LinearLayoutManager(view.context)
ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
ext_recycler.adapter = adapter
ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
}

View File

@ -3,16 +3,15 @@ package eu.kanade.tachiyomi.ui.extension
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.support.v7.preference.*
import android.support.v7.preference.internal.AbstractMultiSelectListPreference
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.DividerItemDecoration.VERTICAL
import android.support.v7.widget.LinearLayoutManager
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.*
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import com.elvishew.xlog.XLog
import com.jakewharton.rxbinding.view.clicks
import eu.kanade.tachiyomi.R
@ -80,7 +79,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore()
manager.onDisplayPreferenceDialogListener = this
val screen = manager.createPreferenceScreen(themedContext)
val screen = manager.createPreferenceScreen(context)
preferenceScreen = screen
val multiSource = extension.sources.size > 1
@ -151,10 +150,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
source.setupPreferenceScreen(newScreen)
for (i in 0 until newScreen.preferenceCount) {
val pref = newScreen.getPreference(i)
// Reparent the preferences
while (newScreen.preferenceCount != 0) {
val pref = newScreen.getPreference(0)
pref.preferenceDataStore = dataStore
pref.order = Int.MAX_VALUE // reset to default order
newScreen.removePreference(pref)
screen.addPreference(pref)
}
}
@ -180,7 +181,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
.newInstance(preference.getKey())
is ListPreference -> ListPreferenceDialogController
.newInstance(preference.getKey())
is AbstractMultiSelectListPreference -> MultiSelectListPreferenceDialogController
is MultiSelectListPreference -> MultiSelectListPreferenceDialogController
.newInstance(preference.getKey())
else -> throw IllegalArgumentException("Tried to display dialog for unknown " +
"preference type. Did you forget to override onDisplayPreferenceDialog()?")
@ -189,8 +190,8 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
f.showDialog(router)
}
override fun findPreference(key: CharSequence?): Preference {
return preferenceScreen!!.getPreference(lastOpenPreferencePosition!!)
override fun <T : Preference> findPreference(key: CharSequence): T? {
return preferenceScreen!!.findPreference(key)
}
override fun loginDialogClosed(source: LoginSource) {

View File

@ -4,10 +4,9 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
class ExtensionDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() {
private val divider: Drawable
@ -17,14 +16,14 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
override fun onDraw(c: Canvas, parent: androidx.recyclerview.widget.RecyclerView, state: androidx.recyclerview.widget.RecyclerView.State) {
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child)
if (holder is ExtensionHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val params = child.layoutParams as androidx.recyclerview.widget.RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
val left = parent.paddingLeft + holder.margin
@ -36,8 +35,8 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView,
state: androidx.recyclerview.widget.RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.extension
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@ -25,14 +24,14 @@ data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderI
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ExtensionGroupHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): ExtensionGroupHolder {
return ExtensionGroupHolder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: ExtensionGroupHolder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: ExtensionGroupHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.extension
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
@ -31,14 +30,14 @@ data class ExtensionItem(val extension: Extension,
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ExtensionHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): ExtensionHolder {
return ExtensionHolder(view, adapter as ExtensionAdapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: ExtensionHolder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: ExtensionHolder,
position: Int, payloads: List<Any?>?) {
if (payloads == null || payloads.isEmpty()) {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
@ -53,7 +51,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
/**
* Recycler view of the list of manga.
*/
private lateinit var recycler: RecyclerView
private lateinit var recycler: androidx.recyclerview.widget.RecyclerView
/**
* Adapter to hold the manga in this category.
@ -80,8 +78,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
this.controller = controller
recycler = if (preferences.libraryAsList().getOrDefault()) {
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
layoutManager = LinearLayoutManager(context)
(swipe_refresh.inflate(R.layout.library_list_recycler) as androidx.recyclerview.widget.RecyclerView).apply {
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
}
} else {
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
@ -95,10 +93,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
recycler.adapter = adapter
swipe_refresh.addView(recycler)
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
recycler.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recycler: androidx.recyclerview.widget.RecyclerView, newState: Int) {
// Disable swipe refresh when view is not at the top
val firstPos = (recycler.layoutManager as LinearLayoutManager)
val firstPos = (recycler.layoutManager as androidx.recyclerview.widget.LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos <= 0
}

View File

@ -5,17 +5,17 @@ import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.f2prateek.rx.preferences.Preference
import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
import com.jakewharton.rxrelay.BehaviorRelay
@ -120,7 +120,7 @@ class LibraryController(
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
private var drawerListener: androidx.drawerlayout.widget.DrawerLayout.DrawerListener? = null
private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
@ -202,10 +202,10 @@ class LibraryController(
super.onDestroyView(view)
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup {
val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
navView = view
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
navView?.onGroupClicked = { group ->
when (group) {
@ -219,7 +219,7 @@ class LibraryController(
return view
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
navView = null
}

View File

@ -1,109 +1,108 @@
package eu.kanade.tachiyomi.ui.library
import android.support.v7.widget.RecyclerView
import android.view.Gravity
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
var downloadCount = -1
override fun getLayoutRes(): Int {
return if (libraryAsList.getOrDefault())
R.layout.catalogue_list_item
else
R.layout.catalogue_grid_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
view.apply {
val coverHeight = parent.itemWidth / 3 * 4
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
}
LibraryGridHolder(view, adapter)
} else {
LibraryListHolder(view, adapter)
}
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: List<Any?>?) {
holder.onSetValues(this)
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
if (constraint.contains(" ") || constraint.contains("\"")) {
val genres = manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
}
var clean_constraint = ""
var ignorespace = false
for (i in constraint.trim().toLowerCase()) {
if (i==' ') {
if (!ignorespace) {
clean_constraint = clean_constraint + ","
} else {
clean_constraint = clean_constraint + " "
}
} else if (i=='"') {
ignorespace = !ignorespace
} else {
clean_constraint = clean_constraint + Character.toString(i)
}
}
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
}
else containsGenre(constraint, manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
})
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
return if (tag.startsWith("-"))
genres?.find {
it.trim().toLowerCase() == tag.substringAfter("-").toLowerCase()
} == null
else
genres?.find {
it.trim().toLowerCase() == tag.toLowerCase()
} != null
}
override fun equals(other: Any?): Boolean {
if (other is LibraryItem) {
return manga.id == other.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.id!!.hashCode()
}
}
package eu.kanade.tachiyomi.ui.library
import android.view.Gravity
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
var downloadCount = -1
override fun getLayoutRes(): Int {
return if (libraryAsList.getOrDefault())
R.layout.catalogue_list_item
else
R.layout.catalogue_grid_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): LibraryHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
view.apply {
val coverHeight = parent.itemWidth / 3 * 4
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
}
LibraryGridHolder(view, adapter)
} else {
LibraryListHolder(view, adapter)
}
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: List<Any?>?) {
holder.onSetValues(this)
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
if (constraint.contains(" ") || constraint.contains("\"")) {
val genres = manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
}
var clean_constraint = ""
var ignorespace = false
for (i in constraint.trim().toLowerCase()) {
if (i==' ') {
if (!ignorespace) {
clean_constraint = clean_constraint + ","
} else {
clean_constraint = clean_constraint + " "
}
} else if (i=='"') {
ignorespace = !ignorespace
} else {
clean_constraint = clean_constraint + Character.toString(i)
}
}
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
}
else containsGenre(constraint, manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
})
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
return if (tag.startsWith("-"))
genres?.find {
it.trim().toLowerCase() == tag.substringAfter("-").toLowerCase()
} == null
else
genres?.find {
it.trim().toLowerCase() == tag.toLowerCase()
} != null
}
override fun equals(other: Any?): Boolean {
if (other is LibraryItem) {
return manga.id == other.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.id!!.hashCode()
}
}

View File

@ -3,19 +3,21 @@ package eu.kanade.tachiyomi.ui.main
import android.animation.ObjectAnimator
import android.app.ActivityManager
import android.app.SearchManager
import android.app.usage.UsageStatsManager
import android.app.Service
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.support.v4.view.GravityCompat
import android.support.v4.widget.DrawerLayout
import android.support.v7.graphics.drawable.DrawerArrowDrawable
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.bluelinelabs.conductor.*
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -32,21 +34,19 @@ import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.util.openInBrowser
import eu.kanade.tachiyomi.util.vibrate
import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker
import exh.uconfig.WarnConfigureDialogController
import exh.ui.batchadd.BatchAddController
import exh.ui.lock.LockChangeHandler
import exh.ui.lock.LockController
import exh.ui.lock.lockEnabled
import exh.ui.lock.notifyLockSecurity
import kotlinx.android.synthetic.main.main_activity.*
import uy.kohesive.injekt.injectLazy
import android.text.TextUtils
import android.view.View
import eu.kanade.tachiyomi.util.vibrate
import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker
import exh.ui.migration.MetadataFetchDialog
import kotlinx.android.synthetic.main.main_activity.*
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.util.*
import kotlin.collections.ArrayList
@ -357,9 +357,9 @@ class MainActivity : BaseActivity() {
val showHamburger = router.backstackSize == 1
if (showHamburger) {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED)
} else {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
// --> EH

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.main
import android.animation.ObjectAnimator
import android.support.design.widget.TabLayout
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import com.google.android.material.tabs.TabLayout
class TabsAnimator(val tabs: TabLayout) {

View File

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

View File

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

View File

@ -10,11 +10,11 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.support.v4.content.pm.ShortcutInfoCompat
import android.support.v4.content.pm.ShortcutManagerCompat
import android.support.v4.graphics.drawable.IconCompat
import android.view.*
import android.widget.Toast
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -403,13 +403,13 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
try {
// --> EH
val urlString = source.mangaDetailsRequest(presenter.manga).url().toString()
val urlString = source.mangaDetailsRequest(presenter.manga).url.toString()
if(preferences.eh_incogWebview().getOrDefault()) {
activity?.startActivity(Intent(activity, WebViewActivity::class.java).apply {
putExtra(WebViewActivity.KEY_URL, urlString)
})
} else {
context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url.toString())
}
// <-- EH
} catch (e: Exception) {
@ -421,7 +421,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
val source = presenter.source as? HttpSource ?: return
val url = try {
source.mangaDetailsRequest(presenter.manga).url().toString()
source.mangaDetailsRequest(presenter.manga).url.toString()
} catch (e: Exception) {
return
}
@ -438,7 +438,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
val source = presenter.source as? HttpSource ?: return
try {
val url = source.mangaDetailsRequest(presenter.manga).url().toString()
val url = source.mangaDetailsRequest(presenter.manga).url.toString()
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.migration
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -14,11 +13,11 @@ class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() {
return R.layout.catalogue_list_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): MangaHolder {
return MangaHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
holder: MangaHolder,
position: Int,
payloads: List<Any?>?) {

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.migration
import android.app.Dialog
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -52,7 +51,7 @@ class MigrationController : NucleusController<MigrationPresenter>(),
super.onViewCreated(view)
adapter = FlexibleAdapter(null, this)
migration_recycler.layoutManager = LinearLayoutManager(view.context)
migration_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
migration_recycler.adapter = adapter
}

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.ui.migration
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.title
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
/**
* Item that contains the selection header.
@ -24,14 +23,14 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return SelectionHeader.Holder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder,
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder,
position: Int, payloads: List<Any?>?) {
// Intentionally empty
}

Some files were not shown because too many files have changed in this diff Show More