mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 16:51:49 +01:00
Merge remote-tracking branch 'upstream/master' into Automatic_Reader_Background
This commit is contained in:
commit
a7cb651df2
@ -64,7 +64,7 @@ Catalogue requests should be created at https://github.com/inorichi/tachiyomi-ex
|
|||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
|
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
|
||||||
You can also reach out to us on [Discord](https://discord.gg/WrBkRk4).
|
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@ -27,7 +28,12 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||||
<!--suppress AndroidDomInspection -->
|
<!--suppress AndroidDomInspection -->
|
||||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
|
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
|
||||||
</activity>
|
</activity>
|
||||||
@ -51,6 +57,21 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.ShikomoriLoginActivity"
|
||||||
|
android:label="Shikomori">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="shikimori-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||||
|
@ -15,6 +15,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
|
const val trueColor = "pref_true_color_key"
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
const val fullscreen = "fullscreen"
|
||||||
|
|
||||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||||
@ -105,6 +107,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
const val downloadBadge = "display_download_badge"
|
const val downloadBadge = "display_download_badge"
|
||||||
|
|
||||||
@Deprecated("Use the preferences of the source")
|
@Deprecated("Use the preferences of the source")
|
||||||
|
@ -43,6 +43,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
|
fun trueColor() = rxPrefs.getBoolean(Keys.trueColor, false)
|
||||||
|
|
||||||
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
|
|
||||||
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
||||||
@ -165,6 +167,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
||||||
|
|
||||||
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikomori.Shikomori
|
||||||
|
|
||||||
class TrackManager(private val context: Context) {
|
class TrackManager(private val context: Context) {
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ class TrackManager(private val context: Context) {
|
|||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
const val ANILIST = 2
|
const val ANILIST = 2
|
||||||
const val KITSU = 3
|
const val KITSU = 3
|
||||||
|
const val SHIKOMORI = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||||
@ -19,7 +21,9 @@ class TrackManager(private val context: Context) {
|
|||||||
|
|
||||||
val kitsu = Kitsu(context, KITSU)
|
val kitsu = Kitsu(context, KITSU)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu)
|
val shikomori = Shikomori(context, SHIKOMORI)
|
||||||
|
|
||||||
|
val services = listOf(myAnimeList, aniList, kitsu, shikomori)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
@ -114,23 +114,23 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).delete()
|
preferences.trackToken(this).delete()
|
||||||
networkService.cookies.remove(URI(BASE_URL))
|
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isLogged: Boolean
|
override val isLogged: Boolean
|
||||||
get() = !getUsername().isEmpty() &&
|
get() = !getUsername().isEmpty() &&
|
||||||
!getPassword().isEmpty() &&
|
!getPassword().isEmpty() &&
|
||||||
checkCookies(URI(BASE_URL)) &&
|
checkCookies() &&
|
||||||
!getCSRF().isEmpty()
|
!getCSRF().isEmpty()
|
||||||
|
|
||||||
private fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
private fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||||
|
|
||||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||||
|
|
||||||
private fun checkCookies(uri: URI): Boolean {
|
private fun checkCookies(): Boolean {
|
||||||
var ckCount = 0
|
var ckCount = 0
|
||||||
|
val url = HttpUrl.parse(BASE_URL)!!
|
||||||
for (ck in networkService.cookies.get(uri)) {
|
for (ck in networkService.cookieManager.get(url)) {
|
||||||
if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
|
if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
|
||||||
ckCount++
|
ckCount++
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikomori
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
// Access token lives 1 day
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikomori
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> {
|
||||||
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
return track.score.toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addLibManga(track, getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateLibManga(track, getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return api.findLibManga(track, getUsername())
|
||||||
|
.flatMap { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.findLibManga(track, getUsername())
|
||||||
|
.map { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLANNING = 5
|
||||||
|
const val REPEATING = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "Shikomori"
|
||||||
|
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
private val interceptor by lazy { ShikomoriInterceptor(this, gson) }
|
||||||
|
|
||||||
|
private val api by lazy { ShikomoriApi(client, interceptor) }
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.shikomori
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLANNING -> getString(R.string.plan_to_read)
|
||||||
|
REPEATING -> getString(R.string.repeating)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(code: String): Completable {
|
||||||
|
return api.accessToken(code).map { oauth: OAuth? ->
|
||||||
|
interceptor.newAuth(oauth)
|
||||||
|
if (oauth != null) {
|
||||||
|
val user = api.getCurrentUser()
|
||||||
|
saveCredentials(user.toString(), oauth.access_token)
|
||||||
|
}
|
||||||
|
}.doOnError {
|
||||||
|
logout()
|
||||||
|
}.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(oauth: OAuth?) {
|
||||||
|
val json = gson.toJson(oauth)
|
||||||
|
preferences.trackToken(this).set(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreToken(): OAuth? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
preferences.trackToken(this).set(null)
|
||||||
|
interceptor.newAuth(null)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikomori
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
|
import com.github.salomonbrys.kotson.nullString
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
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 rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInterceptor) {
|
||||||
|
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
private val parser = JsonParser()
|
||||||
|
private val jsonime = MediaType.parse("application/json; charset=utf-8")
|
||||||
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
|
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
||||||
|
val payload = jsonObject(
|
||||||
|
"user_rate" to jsonObject(
|
||||||
|
"user_id" to user_id,
|
||||||
|
"target_id" to track.media_id,
|
||||||
|
"target_type" to "Manga",
|
||||||
|
"chapters" to track.last_chapter_read,
|
||||||
|
"score" to track.score.toInt(),
|
||||||
|
"status" to track.toShikomoriStatus()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val body = RequestBody.create(jsonime, payload.toString())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiUrl/v2/user_rates")
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map {
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||||
|
|
||||||
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
|
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||||
|
.appendQueryParameter("order", "popularity")
|
||||||
|
.appendQueryParameter("search", search)
|
||||||
|
.appendQueryParameter("limit", "20")
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url.toString())
|
||||||
|
.get()
|
||||||
|
.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).array
|
||||||
|
response.map { jsonToSearch(it.obj) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
|
return TrackSearch.create(TrackManager.SHIKOMORI).apply {
|
||||||
|
media_id = obj["id"].asInt
|
||||||
|
title = obj["name"].asString
|
||||||
|
total_chapters = obj["chapters"].asInt
|
||||||
|
cover_url = baseUrl + obj["image"].obj["preview"].asString
|
||||||
|
summary = ""
|
||||||
|
tracking_url = baseUrl + obj["url"].asString
|
||||||
|
publishing_status = obj["status"].asString
|
||||||
|
publishing_type = obj["kind"].asString
|
||||||
|
start_date = obj.get("aired_on").nullString.orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToTrack(obj: JsonObject): Track {
|
||||||
|
return Track.create(TrackManager.SHIKOMORI).apply {
|
||||||
|
media_id = obj["id"].asInt
|
||||||
|
title = ""
|
||||||
|
last_chapter_read = obj["chapters"].asInt
|
||||||
|
total_chapters = obj["chapters"].asInt
|
||||||
|
score = (obj["score"].asInt).toFloat()
|
||||||
|
status = toTrackStatus(obj["status"].asString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||||
|
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||||
|
.appendQueryParameter("user_id", user_id)
|
||||||
|
.appendQueryParameter("target_id", track.media_id.toString())
|
||||||
|
.appendQueryParameter("target_type", "Manga")
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url.toString())
|
||||||
|
.get()
|
||||||
|
.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).array
|
||||||
|
if (response.size() > 1) {
|
||||||
|
throw Exception("Too much mangas in response")
|
||||||
|
}
|
||||||
|
val entry = response.map {
|
||||||
|
jsonToTrack(it.obj)
|
||||||
|
}
|
||||||
|
entry.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(): Int {
|
||||||
|
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()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
gson.fromJson(responseBody, OAuth::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "authorization_code")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("code", code)
|
||||||
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||||
|
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||||
|
|
||||||
|
private const val baseUrl = "https://shikimori.org"
|
||||||
|
private const val apiUrl = "https://shikimori.org/api"
|
||||||
|
private const val oauthUrl = "https://shikimori.org/oauth/token"
|
||||||
|
private const val loginUrl = "https://shikimori.org/oauth/authorize"
|
||||||
|
|
||||||
|
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
||||||
|
private const val baseMangaUrl = "$apiUrl/mangas"
|
||||||
|
|
||||||
|
fun mangaUrl(remoteId: Int): String {
|
||||||
|
return "$baseMangaUrl/$remoteId"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authUrl() =
|
||||||
|
Uri.parse(loginUrl).buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikomori
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = shikomori.restoreToken()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
val currAuth = oauth ?: throw Exception("Not authenticated with Shikomori")
|
||||||
|
|
||||||
|
val refreshToken = currAuth.refresh_token!!
|
||||||
|
|
||||||
|
// Refresh access token if expired.
|
||||||
|
if (currAuth.isExpired()) {
|
||||||
|
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.header("User-Agent", "Tachiyomi")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newAuth(oauth: OAuth?) {
|
||||||
|
this.oauth = oauth
|
||||||
|
shikomori.saveToken(oauth)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikomori
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
|
fun Track.toShikomoriStatus() = when (status) {
|
||||||
|
Shikomori.READING -> "watching"
|
||||||
|
Shikomori.COMPLETED -> "completed"
|
||||||
|
Shikomori.ON_HOLD -> "on_hold"
|
||||||
|
Shikomori.DROPPED -> "dropped"
|
||||||
|
Shikomori.PLANNING -> "planned"
|
||||||
|
Shikomori.REPEATING -> "rewatching"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTrackStatus(status: String) = when (status) {
|
||||||
|
"watching" -> Shikomori.READING
|
||||||
|
"completed" -> Shikomori.COMPLETED
|
||||||
|
"on_hold" -> Shikomori.ON_HOLD
|
||||||
|
"dropped" -> Shikomori.DROPPED
|
||||||
|
"planned" -> Shikomori.PLANNING
|
||||||
|
"rewatching" -> Shikomori.REPEATING
|
||||||
|
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.CookieSyncManager
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class AndroidCookieJar(context: Context) : CookieJar {
|
||||||
|
|
||||||
|
private val manager = CookieManager.getInstance()
|
||||||
|
|
||||||
|
private val syncManager by lazy { CookieSyncManager.createInstance(context) }
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Init sync manager when using anything below L
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
syncManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
|
||||||
|
val urlString = url.toString()
|
||||||
|
|
||||||
|
for (cookie in cookies) {
|
||||||
|
manager.setCookie(urlString, cookie.toString())
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
syncManager.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
return get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(url: HttpUrl): List<Cookie> {
|
||||||
|
val cookies = manager.getCookie(url.toString())
|
||||||
|
|
||||||
|
return if (cookies != null && !cookies.isEmpty()) {
|
||||||
|
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(url: HttpUrl) {
|
||||||
|
val cookies = manager.getCookie(url.toString()) ?: return
|
||||||
|
val domain = ".${url.host()}"
|
||||||
|
cookies.split(";")
|
||||||
|
.map { it.substringBefore("=") }
|
||||||
|
.onEach { manager.setCookie(domain, "$it=;Max-Age=-1") }
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
syncManager.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAll() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
manager.removeAllCookies {}
|
||||||
|
} else {
|
||||||
|
manager.removeAllCookie()
|
||||||
|
syncManager.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,41 +1,52 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import com.squareup.duktape.Duktape
|
import android.annotation.SuppressLint
|
||||||
import okhttp3.*
|
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.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class CloudflareInterceptor : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
|
||||||
|
|
||||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
|
||||||
|
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
|
||||||
|
|
||||||
private val sPattern = Regex("""name="s" value="([^"]+)""")
|
|
||||||
|
|
||||||
private val kPattern = Regex("""k\s+=\s+'([^']+)';""")
|
|
||||||
|
|
||||||
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
|
||||||
private interface IBase64 {
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
fun decode(input: String): String
|
|
||||||
}
|
|
||||||
|
|
||||||
private val b64: IBase64 = object : IBase64 {
|
/**
|
||||||
override fun decode(input: String): String {
|
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||||
return okio.ByteString.decodeBase64(input)!!.utf8()
|
* 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
|
@Synchronized
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
initWebView
|
||||||
|
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
||||||
return try {
|
try {
|
||||||
chain.proceed(resolveChallenge(response))
|
response.close()
|
||||||
|
val solutionRequest = resolveWithWebView(chain.request())
|
||||||
|
return chain.proceed(solutionRequest)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
// we don't crash the entire app
|
// we don't crash the entire app
|
||||||
@ -46,65 +57,98 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveChallenge(response: Response): Request {
|
private fun isChallengeSolutionUrl(url: String): Boolean {
|
||||||
Duktape.create().use { duktape ->
|
return "chk_jschl" in url
|
||||||
val originalRequest = response.request()
|
}
|
||||||
val url = originalRequest.url()
|
|
||||||
val domain = url.host()
|
|
||||||
val content = response.body()!!.string()
|
|
||||||
|
|
||||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
Thread.sleep(4000)
|
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)
|
||||||
|
|
||||||
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
var webView: WebView? = null
|
||||||
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
var solutionUrl: String? = null
|
||||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
var challengeFound = false
|
||||||
val s = sPattern.find(content)?.groups?.get(1)?.value
|
|
||||||
|
|
||||||
// If `k` is null, it uses old methods.
|
val origRequestUrl = request.url().toString()
|
||||||
val k = kPattern.find(content)?.groups?.get(1)?.value ?: ""
|
val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||||
val innerHTMLValue = Regex("""<div(.*)id="$k"(.*)>(.*)</div>""")
|
|
||||||
.find(content)?.groups?.get(3)?.value ?: ""
|
|
||||||
|
|
||||||
if (operation == null || challenge == null || pass == null || s == null) {
|
handler.post {
|
||||||
throw Exception("Failed resolving Cloudflare challenge")
|
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)
|
||||||
// Export native Base64 decode function to js object.
|
|
||||||
duktape.set("b64", IBase64::class.java, b64)
|
|
||||||
|
|
||||||
// Return simulated innerHTML when call DOM.
|
|
||||||
val simulatedDocumentJS = """var document = { getElementById: function (x) { return { innerHTML: "$innerHTMLValue" }; } }"""
|
|
||||||
|
|
||||||
val js = operation
|
|
||||||
.replace(Regex("""a\.value = (.+\.toFixed\(10\);).+"""), "$1")
|
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
|
||||||
.replace("t.length", "${domain.length}")
|
|
||||||
.replace("\n", "")
|
|
||||||
|
|
||||||
val result = duktape.evaluate("""$simulatedDocumentJS;$ATOB_JS;var t="$domain";$js""") as String
|
|
||||||
|
|
||||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("jschl_vc", challenge)
|
|
||||||
.addQueryParameter("pass", pass)
|
|
||||||
.addQueryParameter("s", s)
|
|
||||||
.addQueryParameter("jschl_answer", result)
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
val cloudflareHeaders = originalRequest.headers()
|
|
||||||
.newBuilder()
|
|
||||||
.add("Referer", url.toString())
|
|
||||||
.add("Accept", "text/html,application/xhtml+xml,application/xml")
|
|
||||||
.add("Accept-Language", "en")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(cloudflareUrl, cloudflareHeaders, cache = CacheControl.Builder().build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
}
|
||||||
// atob() is browser API, Using Android's own function. (java.util.Base64 can't be used because of min API level)
|
|
||||||
private const val ATOB_JS = """var atob = function (input) { return b64.decode(input) }"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,11 +2,7 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import okhttp3.Cache
|
import okhttp3.*
|
||||||
import okhttp3.CipherSuite
|
|
||||||
import okhttp3.ConnectionSpec
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.TlsVersion
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
@ -15,11 +11,7 @@ import java.net.UnknownHostException
|
|||||||
import java.security.KeyManagementException
|
import java.security.KeyManagementException
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.*
|
||||||
import javax.net.ssl.SSLSocket
|
|
||||||
import javax.net.ssl.SSLSocketFactory
|
|
||||||
import javax.net.ssl.TrustManagerFactory
|
|
||||||
import javax.net.ssl.X509TrustManager
|
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
@ -27,7 +19,7 @@ class NetworkHelper(context: Context) {
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
private val cookieManager = PersistentCookieJar(context)
|
val cookieManager = AndroidCookieJar(context)
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
val client = OkHttpClient.Builder()
|
||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
@ -36,12 +28,9 @@ class NetworkHelper(context: Context) {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
val cloudflareClient = client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor())
|
.addInterceptor(CloudflareInterceptor(context))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cookies: PersistentCookieStore
|
|
||||||
get() = cookieManager.store
|
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
||||||
return this
|
return this
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class PersistentCookieJar(context: Context) : CookieJar {
|
|
||||||
|
|
||||||
val store = PersistentCookieStore(context)
|
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
store.addAll(url, cookies)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
return store.get(url)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import java.net.URI
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
class PersistentCookieStore(context: Context) {
|
|
||||||
|
|
||||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
|
||||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
init {
|
|
||||||
for ((key, value) in prefs.all) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val cookies = value as? Set<String>
|
|
||||||
if (cookies != null) {
|
|
||||||
try {
|
|
||||||
val url = HttpUrl.parse("http://$key") ?: continue
|
|
||||||
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
|
||||||
.filter { !it.hasExpired() }
|
|
||||||
cookieMap.put(key, nonExpiredCookies)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
val key = url.uri().host
|
|
||||||
|
|
||||||
// Append or replace the cookies for this domain.
|
|
||||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
|
||||||
for (cookie in cookies) {
|
|
||||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
|
||||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
|
||||||
if (pos == -1) {
|
|
||||||
cookiesForDomain.add(cookie)
|
|
||||||
} else {
|
|
||||||
cookiesForDomain[pos] = cookie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cookieMap.put(key, cookiesForDomain)
|
|
||||||
|
|
||||||
// Get cookies to be stored in disk
|
|
||||||
val newValues = cookiesForDomain.asSequence()
|
|
||||||
.filter { it.persistent() && !it.hasExpired() }
|
|
||||||
.map(Cookie::toString)
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
prefs.edit().putStringSet(key, newValues).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun removeAll() {
|
|
||||||
prefs.edit().clear().apply()
|
|
||||||
cookieMap.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(uri: URI) {
|
|
||||||
prefs.edit().remove(uri.host).apply()
|
|
||||||
cookieMap.remove(uri.host)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(url: HttpUrl) = get(url.uri().host)
|
|
||||||
|
|
||||||
fun get(uri: URI) = get(uri.host)
|
|
||||||
|
|
||||||
private fun get(url: String): List<Cookie> {
|
|
||||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.main
|
package eu.kanade.tachiyomi.ui.main
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||||
import eu.kanade.tachiyomi.ui.extension.ExtensionController
|
import eu.kanade.tachiyomi.ui.extension.ExtensionController
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||||
@ -158,6 +160,16 @@ class MainActivity : BaseActivity() {
|
|||||||
setSelectedDrawerItem(R.id.nav_drawer_downloads)
|
setSelectedDrawerItem(R.id.nav_drawer_downloads)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> {
|
||||||
|
//If the intent match the "standard" Android search intent
|
||||||
|
// or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant)
|
||||||
|
|
||||||
|
setSelectedDrawerItem(R.id.nav_drawer_catalogues)
|
||||||
|
//Get the search query provided in extras, and if not null, perform a global search with it.
|
||||||
|
intent.getStringExtra(SearchManager.QUERY)?.also { query ->
|
||||||
|
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -15,12 +15,7 @@ import android.support.customtabs.CustomTabsIntent
|
|||||||
import android.support.v4.content.pm.ShortcutInfoCompat
|
import android.support.v4.content.pm.ShortcutInfoCompat
|
||||||
import android.support.v4.content.pm.ShortcutManagerCompat
|
import android.support.v4.content.pm.ShortcutManagerCompat
|
||||||
import android.support.v4.graphics.drawable.IconCompat
|
import android.support.v4.graphics.drawable.IconCompat
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
@ -138,6 +133,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_open_in_browser -> openInBrowser()
|
R.id.action_open_in_browser -> openInBrowser()
|
||||||
|
R.id.action_open_in_web_view -> openInWebView()
|
||||||
R.id.action_share -> shareManga()
|
R.id.action_share -> shareManga()
|
||||||
R.id.action_add_to_home_screen -> addToHomeScreen()
|
R.id.action_add_to_home_screen -> addToHomeScreen()
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
@ -302,6 +298,19 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openInWebView() {
|
||||||
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
|
||||||
|
val url = try {
|
||||||
|
source.mangaDetailsRequest(presenter.manga).url().toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentController?.router?.pushController(MangaWebViewController(source.id, url)
|
||||||
|
.withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class MangaWebViewController(bundle: Bundle? = null) : BaseController(bundle) {
|
||||||
|
|
||||||
|
private val sourceManager by injectLazy<SourceManager>()
|
||||||
|
|
||||||
|
constructor(sourceId: Long, url: String) : this(Bundle().apply {
|
||||||
|
putLong(SOURCE_KEY, sourceId)
|
||||||
|
putString(URL_KEY, url)
|
||||||
|
})
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.manga_info_web_controller, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
val source = sourceManager.get(args.getLong(SOURCE_KEY)) as? HttpSource ?: return
|
||||||
|
val url = args.getString(URL_KEY) ?: return
|
||||||
|
val headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||||
|
|
||||||
|
val web = view as WebView
|
||||||
|
web.settings.javaScriptEnabled = true
|
||||||
|
web.settings.userAgentString = source.headers["User-Agent"]
|
||||||
|
web.loadUrl(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
val web = view as WebView
|
||||||
|
web.stopLoading()
|
||||||
|
web.destroy()
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val SOURCE_KEY = "source_key"
|
||||||
|
const val URL_KEY = "url_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -13,6 +14,7 @@ import android.view.*
|
|||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -558,6 +560,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
subscriptions += preferences.showPageNumber().asObservable()
|
subscriptions += preferences.showPageNumber().asObservable()
|
||||||
.subscribe { setPageNumberVisibility(it) }
|
.subscribe { setPageNumberVisibility(it) }
|
||||||
|
|
||||||
|
subscriptions += preferences.trueColor().asObservable()
|
||||||
|
.subscribe { setTrueColor(it) }
|
||||||
|
|
||||||
subscriptions += preferences.fullscreen().asObservable()
|
subscriptions += preferences.fullscreen().asObservable()
|
||||||
.subscribe { setFullscreen(it) }
|
.subscribe { setFullscreen(it) }
|
||||||
|
|
||||||
@ -614,6 +619,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
page_number.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
page_number.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the 32-bit color mode according to [enabled].
|
||||||
|
*/
|
||||||
|
private fun setTrueColor(enabled: Boolean) {
|
||||||
|
if (enabled)
|
||||||
|
SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.ARGB_8888)
|
||||||
|
else
|
||||||
|
SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.RGB_565)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the fullscreen reading mode (immersive) according to [enabled].
|
* Sets the fullscreen reading mode (immersive) according to [enabled].
|
||||||
*/
|
*/
|
||||||
|
@ -32,7 +32,7 @@ import timber.log.Timber
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,12 +84,25 @@ class ReaderPresenter(
|
|||||||
private val chapterList by lazy {
|
private val chapterList by lazy {
|
||||||
val manga = manga!!
|
val manga = manga!!
|
||||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
val selectedChapter = dbChapters.find { it.id == chapterId }
|
val selectedChapter = dbChapters.find { it.id == chapterId }
|
||||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||||
|
|
||||||
|
val chaptersForReader =
|
||||||
|
if (preferences.skipRead()) {
|
||||||
|
var list = dbChapters.filter { it -> !it.read }.toMutableList()
|
||||||
|
val find = list.find { it.id == chapterId }
|
||||||
|
if (find == null) {
|
||||||
|
list.add(selectedChapter)
|
||||||
|
}
|
||||||
|
list
|
||||||
|
} else {
|
||||||
|
dbChapters
|
||||||
|
}
|
||||||
|
|
||||||
when (manga.sorting) {
|
when (manga.sorting) {
|
||||||
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(dbChapters)
|
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader)
|
||||||
Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(dbChapters, selectedChapter)
|
Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(chaptersForReader, selectedChapter)
|
||||||
else -> error("Unknown sorting method")
|
else -> error("Unknown sorting method")
|
||||||
}.map(::ReaderChapter)
|
}.map(::ReaderChapter)
|
||||||
}
|
}
|
||||||
@ -165,12 +178,12 @@ class ReaderPresenter(
|
|||||||
if (!needsInit()) return
|
if (!needsInit()) return
|
||||||
|
|
||||||
db.getManga(mangaId).asRxObservable()
|
db.getManga(mangaId).asRxObservable()
|
||||||
.first()
|
.first()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { init(it, initialChapterId) }
|
.doOnNext { init(it, initialChapterId) }
|
||||||
.subscribeFirst({ _, _ ->
|
.subscribeFirst({ _, _ ->
|
||||||
// Ignore onNext event
|
// Ignore onNext event
|
||||||
}, ReaderActivity::setInitialChapterError)
|
}, ReaderActivity::setInitialChapterError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -193,13 +206,13 @@ class ReaderPresenter(
|
|||||||
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = Observable
|
activeChapterSubscription = Observable
|
||||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||||
.flatMap { getLoadObservable(loader!!, it) }
|
.flatMap { getLoadObservable(loader!!, it) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ _, _ ->
|
.subscribeFirst({ _, _ ->
|
||||||
// Ignore onNext event
|
// Ignore onNext event
|
||||||
}, ReaderActivity::setInitialChapterError)
|
}, ReaderActivity::setInitialChapterError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,23 +227,23 @@ class ReaderPresenter(
|
|||||||
chapter: ReaderChapter
|
chapter: ReaderChapter
|
||||||
): Observable<ViewerChapters> {
|
): Observable<ViewerChapters> {
|
||||||
return loader.loadChapter(chapter)
|
return loader.loadChapter(chapter)
|
||||||
.andThen(Observable.fromCallable {
|
.andThen(Observable.fromCallable {
|
||||||
val chapterPos = chapterList.indexOf(chapter)
|
val chapterPos = chapterList.indexOf(chapter)
|
||||||
|
|
||||||
ViewerChapters(chapter,
|
ViewerChapters(chapter,
|
||||||
chapterList.getOrNull(chapterPos - 1),
|
chapterList.getOrNull(chapterPos - 1),
|
||||||
chapterList.getOrNull(chapterPos + 1))
|
chapterList.getOrNull(chapterPos + 1))
|
||||||
})
|
})
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { newChapters ->
|
.doOnNext { newChapters ->
|
||||||
val oldChapters = viewerChaptersRelay.value
|
val oldChapters = viewerChaptersRelay.value
|
||||||
|
|
||||||
// Add new references first to avoid unnecessary recycling
|
// Add new references first to avoid unnecessary recycling
|
||||||
newChapters.ref()
|
newChapters.ref()
|
||||||
oldChapters?.unref()
|
oldChapters?.unref()
|
||||||
|
|
||||||
viewerChaptersRelay.call(newChapters)
|
viewerChaptersRelay.call(newChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,10 +257,10 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||||
.toCompletable()
|
.toCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.also(::add)
|
.also(::add)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -262,13 +275,13 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||||
.subscribeFirst({ view, _ ->
|
.subscribeFirst({ view, _ ->
|
||||||
view.moveToPageIndex(0)
|
view.moveToPageIndex(0)
|
||||||
}, { _, _ ->
|
}, { _, _ ->
|
||||||
// Ignore onError event, viewers handle that state
|
// Ignore onError event, viewers handle that state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,12 +298,12 @@ class ReaderPresenter(
|
|||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
|
|
||||||
loader.loadChapter(chapter)
|
loader.loadChapter(chapter)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Update current chapters whenever a chapter is preloaded
|
// Update current chapters whenever a chapter is preloaded
|
||||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.also(::add)
|
.also(::add)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -331,9 +344,9 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private fun saveChapterProgress(chapter: ReaderChapter) {
|
private fun saveChapterProgress(chapter: ReaderChapter) {
|
||||||
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -342,9 +355,9 @@ class ReaderPresenter(
|
|||||||
private fun saveChapterHistory(chapter: ReaderChapter) {
|
private fun saveChapterHistory(chapter: ReaderChapter) {
|
||||||
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
||||||
db.updateHistoryLastRead(history).asRxCompletable()
|
db.updateHistoryLastRead(history).asRxCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -394,18 +407,18 @@ class ReaderPresenter(
|
|||||||
db.updateMangaViewer(manga).executeAsBlocking()
|
db.updateMangaViewer(manga).executeAsBlocking()
|
||||||
|
|
||||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ view, _ ->
|
.subscribeFirst({ view, _ ->
|
||||||
val currChapters = viewerChaptersRelay.value
|
val currChapters = viewerChaptersRelay.value
|
||||||
if (currChapters != null) {
|
if (currChapters != null) {
|
||||||
// Save current page
|
// Save current page
|
||||||
val currChapter = currChapters.currChapter
|
val currChapter = currChapters.currChapter
|
||||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||||
|
|
||||||
// Emit manga and chapters to the new viewer
|
// Emit manga and chapters to the new viewer
|
||||||
view.setManga(manga)
|
view.setManga(manga)
|
||||||
view.setChapters(currChapters)
|
view.setChapters(currChapters)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -446,22 +459,22 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
// Pictures directory.
|
// Pictures directory.
|
||||||
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
File.separator + Environment.DIRECTORY_PICTURES +
|
||||||
File.separator + "Tachiyomi")
|
File.separator + "Tachiyomi")
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||||
.doOnNext { file ->
|
.doOnNext { file ->
|
||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, file)
|
||||||
notifier.onComplete(file)
|
notifier.onComplete(file)
|
||||||
}
|
}
|
||||||
.doOnError { notifier.onError(it.message) }
|
.doOnError { notifier.onError(it.message) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -478,14 +491,14 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
val destDir = File(context.cacheDir, "shared_image")
|
val destDir = File(context.cacheDir, "shared_image")
|
||||||
|
|
||||||
Observable.fromCallable { destDir.delete() } // Keep only the last shared file
|
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||||
.map { saveImage(page, destDir, manga) }
|
.map { saveImage(page, destDir, manga) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, file -> view.onShareImageResult(file) },
|
{ view, file -> view.onShareImageResult(file) },
|
||||||
{ view, error -> /* Empty */ }
|
{ view, error -> /* Empty */ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -497,28 +510,28 @@ class ReaderPresenter(
|
|||||||
val stream = page.stream ?: return
|
val stream = page.stream ?: return
|
||||||
|
|
||||||
Observable
|
Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (manga.source == LocalSource.ID) {
|
if (manga.source == LocalSource.ID) {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
LocalSource.updateCover(context, manga, stream())
|
LocalSource.updateCover(context, manga, stream())
|
||||||
R.string.cover_updated
|
R.string.cover_updated
|
||||||
SetAsCoverResult.Success
|
|
||||||
} else {
|
|
||||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
|
||||||
if (manga.favorite) {
|
|
||||||
coverCache.copyToCache(thumbUrl, stream())
|
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||||
|
if (manga.favorite) {
|
||||||
|
coverCache.copyToCache(thumbUrl, stream())
|
||||||
|
SetAsCoverResult.Success
|
||||||
|
} else {
|
||||||
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribeOn(Schedulers.io())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeFirst(
|
||||||
.subscribeFirst(
|
{ view, result -> view.onSetAsCoverResult(result) },
|
||||||
{ view, result -> view.onSetAsCoverResult(result) },
|
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -559,26 +572,26 @@ class ReaderPresenter(
|
|||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
|
||||||
db.getTracks(manga).asRxSingle()
|
db.getTracks(manga).asRxSingle()
|
||||||
.flatMapCompletable { trackList ->
|
.flatMapCompletable { trackList ->
|
||||||
Completable.concat(trackList.map { track ->
|
Completable.concat(trackList.map { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
|
if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
|
||||||
track.last_chapter_read = lastChapterRead
|
track.last_chapter_read = lastChapterRead
|
||||||
|
|
||||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
// We wan't these to execute even if the presenter is destroyed and leaks
|
||||||
// for a while. The view can still be garbage collected.
|
// for a while. The view can still be garbage collected.
|
||||||
Observable.defer { service.update(track) }
|
Observable.defer { service.update(track) }
|
||||||
.map { db.insertTrack(track).executeAsBlocking() }
|
.map { db.insertTrack(track).executeAsBlocking() }
|
||||||
.toCompletable()
|
.toCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
} else {
|
} else {
|
||||||
Completable.complete()
|
Completable.complete()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -594,19 +607,19 @@ class ReaderPresenter(
|
|||||||
if (removeAfterReadSlots == -1) return
|
if (removeAfterReadSlots == -1) return
|
||||||
|
|
||||||
Completable
|
Completable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
// Position of the read chapter
|
// Position of the read chapter
|
||||||
val position = chapterList.indexOf(chapter)
|
val position = chapterList.indexOf(chapter)
|
||||||
|
|
||||||
// Retrieve chapter to delete according to preference
|
// Retrieve chapter to delete according to preference
|
||||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||||
if (chapterToDelete != null) {
|
if (chapterToDelete != null) {
|
||||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.onErrorComplete()
|
||||||
.onErrorComplete()
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribe()
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -615,9 +628,9 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private fun deletePendingChapters() {
|
private fun deletePendingChapters() {
|
||||||
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
titleRes = R.string.pref_clear_cookies
|
titleRes = R.string.pref_clear_cookies
|
||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
network.cookies.removeAll()
|
network.cookieManager.removeAll()
|
||||||
activity?.toast(R.string.cookies_cleared)
|
activity?.toast(R.string.cookies_cleared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.support.v7.preference.PreferenceScreen
|
import android.support.v7.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
@ -62,6 +63,11 @@ class SettingsReaderController : SettingsController() {
|
|||||||
defaultValue = "500"
|
defaultValue = "500"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.skipRead
|
||||||
|
titleRes = R.string.pref_skip_read_chapters
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = Keys.fullscreen
|
key = Keys.fullscreen
|
||||||
titleRes = R.string.pref_fullscreen
|
titleRes = R.string.pref_fullscreen
|
||||||
@ -77,6 +83,13 @@ class SettingsReaderController : SettingsController() {
|
|||||||
titleRes = R.string.pref_show_page_number
|
titleRes = R.string.pref_show_page_number
|
||||||
defaultValue = true
|
defaultValue = true
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.trueColor
|
||||||
|
titleRes = R.string.pref_true_color
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.pager_viewer
|
titleRes = R.string.pager_viewer
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikomori.ShikomoriApi
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||||
@ -53,6 +54,15 @@ class SettingsTrackingController : SettingsController(),
|
|||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trackPreference(trackManager.shikomori) {
|
||||||
|
onClick {
|
||||||
|
val tabsIntent = CustomTabsIntent.Builder()
|
||||||
|
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||||
|
.build()
|
||||||
|
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||||
|
tabsIntent.launchUrl(activity, ShikomoriApi.authUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +80,7 @@ class SettingsTrackingController : SettingsController(),
|
|||||||
super.onActivityResumed(activity)
|
super.onActivityResumed(activity)
|
||||||
// Manually refresh anilist holder
|
// Manually refresh anilist holder
|
||||||
updatePreference(trackManager.aniList.id)
|
updatePreference(trackManager.aniList.id)
|
||||||
|
updatePreference(trackManager.shikomori.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePreference(id: Int) {
|
private fun updatePreference(id: Int) {
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.view.Gravity.CENTER
|
||||||
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class ShikomoriLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
val view = ProgressBar(this)
|
||||||
|
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||||
|
|
||||||
|
val code = intent.data?.getQueryParameter("code")
|
||||||
|
if (code != null) {
|
||||||
|
trackManager.shikomori.login(code)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({
|
||||||
|
returnToSettings()
|
||||||
|
}, {
|
||||||
|
returnToSettings()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
trackManager.shikomori.logout()
|
||||||
|
returnToSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun returnToSettings() {
|
||||||
|
finish()
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.os.Build
|
||||||
|
import android.webkit.*
|
||||||
|
|
||||||
|
@Suppress("OverridingDeprecatedMember")
|
||||||
|
abstract class WebViewClientCompat : WebViewClient() {
|
||||||
|
|
||||||
|
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onReceivedErrorCompat(
|
||||||
|
view: WebView,
|
||||||
|
errorCode: Int,
|
||||||
|
description: String?,
|
||||||
|
failingUrl: String,
|
||||||
|
isMainFrame: Boolean) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
final override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||||
|
return shouldOverrideUrlCompat(view, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
final override fun shouldInterceptRequest(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): WebResourceResponse? {
|
||||||
|
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun shouldInterceptRequest(
|
||||||
|
view: WebView,
|
||||||
|
url: String
|
||||||
|
): WebResourceResponse? {
|
||||||
|
return shouldInterceptRequestCompat(view, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
final override fun onReceivedError(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest,
|
||||||
|
error: WebResourceError
|
||||||
|
) {
|
||||||
|
onReceivedErrorCompat(view, error.errorCode, error.description?.toString(),
|
||||||
|
request.url.toString(), request.isForMainFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onReceivedError(
|
||||||
|
view: WebView,
|
||||||
|
errorCode: Int,
|
||||||
|
description: String?,
|
||||||
|
failingUrl: String
|
||||||
|
) {
|
||||||
|
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
final override fun onReceivedHttpError(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest,
|
||||||
|
error: WebResourceResponse
|
||||||
|
) {
|
||||||
|
onReceivedErrorCompat(view, error.statusCode, error.reasonPhrase, request.url
|
||||||
|
.toString(), request.isForMainFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
app/src/main/res/drawable-xxxhdpi/shikomori.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/shikomori.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
7
app/src/main/res/layout/manga_info_web_controller.xml
Normal file
7
app/src/main/res/layout/manga_info_web_controller.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<WebView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
</WebView>
|
@ -105,6 +105,17 @@
|
|||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/background_color" />
|
app:layout_constraintTop_toBottomOf="@id/background_color" />
|
||||||
|
|
||||||
|
<android.support.v7.widget.SwitchCompat
|
||||||
|
android:id="@+id/true_color"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/pref_true_color"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/show_page_number" />
|
||||||
|
|
||||||
<android.support.v7.widget.SwitchCompat
|
<android.support.v7.widget.SwitchCompat
|
||||||
android:id="@+id/fullscreen"
|
android:id="@+id/fullscreen"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -112,7 +123,7 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/pref_fullscreen"
|
android:text="@string/pref_fullscreen"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/show_page_number" />
|
app:layout_constraintTop_toBottomOf="@id/true_color" />
|
||||||
|
|
||||||
<android.support.v7.widget.SwitchCompat
|
<android.support.v7.widget.SwitchCompat
|
||||||
android:id="@+id/keepscreen"
|
android:id="@+id/keepscreen"
|
||||||
|
@ -12,8 +12,12 @@
|
|||||||
android:title="@string/action_open_in_browser"
|
android:title="@string/action_open_in_browser"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/action_open_in_web_view"
|
||||||
|
android:title="@string/action_open_in_web_view"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
<item android:id="@+id/action_add_to_home_screen"
|
<item android:id="@+id/action_add_to_home_screen"
|
||||||
android:title="@string/action_add_to_home_screen"
|
android:title="@string/action_add_to_home_screen"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -73,6 +73,7 @@
|
|||||||
<string name="action_resume">Resume</string>
|
<string name="action_resume">Resume</string>
|
||||||
<string name="action_move">Move</string>
|
<string name="action_move">Move</string>
|
||||||
<string name="action_open_in_browser">Open in browser</string>
|
<string name="action_open_in_browser">Open in browser</string>
|
||||||
|
<string name="action_open_in_web_view">Open in web view</string>
|
||||||
<string name="action_add_to_home_screen">Add to home screen</string>
|
<string name="action_add_to_home_screen">Add to home screen</string>
|
||||||
<string name="action_display_mode">Change display mode</string>
|
<string name="action_display_mode">Change display mode</string>
|
||||||
<string name="action_display">Display</string>
|
<string name="action_display">Display</string>
|
||||||
@ -173,10 +174,12 @@
|
|||||||
<string name="pref_page_transitions">Page transitions</string>
|
<string name="pref_page_transitions">Page transitions</string>
|
||||||
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
||||||
<string name="pref_show_page_number">Show page number</string>
|
<string name="pref_show_page_number">Show page number</string>
|
||||||
|
<string name="pref_true_color">32-bit color</string>
|
||||||
<string name="pref_crop_borders">Crop borders</string>
|
<string name="pref_crop_borders">Crop borders</string>
|
||||||
<string name="pref_custom_brightness">Use custom brightness</string>
|
<string name="pref_custom_brightness">Use custom brightness</string>
|
||||||
<string name="pref_custom_color_filter">Use custom color filter</string>
|
<string name="pref_custom_color_filter">Use custom color filter</string>
|
||||||
<string name="pref_keep_screen_on">Keep screen on</string>
|
<string name="pref_keep_screen_on">Keep screen on</string>
|
||||||
|
<string name="pref_skip_read_chapters">Skip chapters marked read</string>
|
||||||
<string name="pref_reader_navigation">Navigation</string>
|
<string name="pref_reader_navigation">Navigation</string>
|
||||||
<string name="pref_read_with_volume_keys">Volume keys</string>
|
<string name="pref_read_with_volume_keys">Volume keys</string>
|
||||||
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
|
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
|
||||||
|
4
app/src/main/res/xml/backup_rules.xml
Normal file
4
app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<include domain="database" path="tachiyomi.db"/>
|
||||||
|
</full-backup-content>
|
5
app/src/main/res/xml/searchable.xml
Normal file
5
app/src/main/res/xml/searchable.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:hint="@string/action_global_search_hint" >
|
||||||
|
</searchable>
|
Loading…
Reference in New Issue
Block a user