From af0e3a278fba8c82a6e4c05a2c609e3eb4f3fb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Holl=C3=BD?= Date: Sat, 30 Mar 2019 11:39:10 +0100 Subject: [PATCH 01/11] Fix discord link (#1951) Fix discord link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92b9e80b70..4b21282d6b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Catalogue requests should be created at https://github.com/inorichi/tachiyomi-ex ## 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 From e47dd3d587eff58d57e4df62f149c94bc2753f4a Mon Sep 17 00:00:00 2001 From: Deumiankio <39210184+Deumiankio@users.noreply.github.com> Date: Sat, 30 Mar 2019 14:21:35 +0100 Subject: [PATCH 02/11] Add 32-bit color mode to reader settings (#1941) * add ARGB_8888 mode to reader settings * Only show option on Oreo or later. Only show option in settings screen. --- .../tachiyomi/data/preference/PreferenceKeys.kt | 2 ++ .../data/preference/PreferencesHelper.kt | 2 ++ .../kanade/tachiyomi/ui/reader/ReaderActivity.kt | 15 +++++++++++++++ .../ui/setting/SettingsReaderController.kt | 8 ++++++++ app/src/main/res/layout/reader_settings_sheet.xml | 13 ++++++++++++- app/src/main/res/values/strings.xml | 1 + 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 259e1fb8c2..a3d5297f66 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -15,6 +15,8 @@ object PreferenceKeys { const val showPageNumber = "pref_show_page_number_key" + const val trueColor = "pref_true_color_key" + const val fullscreen = "fullscreen" const val keepScreenOn = "pref_keep_screen_on_key" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index ee5b0b39e5..c7b908f55e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -43,6 +43,8 @@ class PreferencesHelper(val context: Context) { fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true) + fun trueColor() = rxPrefs.getBoolean(Keys.trueColor, false) + fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true) fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 9861e288cb..c3b8aefd80 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration +import android.graphics.Bitmap import android.graphics.Color import android.os.Build import android.os.Bundle @@ -13,6 +14,7 @@ import android.view.* import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.SeekBar +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -558,6 +560,9 @@ class ReaderActivity : BaseRxActivity() { subscriptions += preferences.showPageNumber().asObservable() .subscribe { setPageNumberVisibility(it) } + subscriptions += preferences.trueColor().asObservable() + .subscribe { setTrueColor(it) } + subscriptions += preferences.fullscreen().asObservable() .subscribe { setFullscreen(it) } @@ -614,6 +619,16 @@ class ReaderActivity : BaseRxActivity() { 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]. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 9439953850..ac413f270e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.setting +import android.os.Build import android.support.v7.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys @@ -77,6 +78,13 @@ class SettingsReaderController : SettingsController() { titleRes = R.string.pref_show_page_number defaultValue = true } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + switchPreference { + key = Keys.trueColor + titleRes = R.string.pref_true_color + defaultValue = false + } + } preferenceCategory { titleRes = R.string.pager_viewer diff --git a/app/src/main/res/layout/reader_settings_sheet.xml b/app/src/main/res/layout/reader_settings_sheet.xml index f3b402baa3..d28155d704 100644 --- a/app/src/main/res/layout/reader_settings_sheet.xml +++ b/app/src/main/res/layout/reader_settings_sheet.xml @@ -105,6 +105,17 @@ android:textColor="?android:attr/textColorSecondary" app:layout_constraintTop_toBottomOf="@id/background_color" /> + + + app:layout_constraintTop_toBottomOf="@id/true_color" /> Page transitions Double tap animation speed Show page number + 32-bit color Crop borders Use custom brightness Use custom color filter From 55bf1c31a62730f18668485e0abc92846f05b0cb Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 1 Apr 2019 17:14:37 +0200 Subject: [PATCH 03/11] Set explicit autobackup rules --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/backup_rules.xml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 app/src/main/res/xml/backup_rules.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6537290437..6c8f6aea26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + + + From f1f6a2b341ea554eb98ae797594b930e7d0f2f71 Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 1 Apr 2019 17:20:13 +0200 Subject: [PATCH 04/11] Test solving Cloudflare's challenge with WebView --- .../data/track/myanimelist/MyAnimeList.kt | 12 +- .../tachiyomi/network/AndroidCookieJar.kt | 63 +++++++ .../network/CloudflareInterceptor.kt | 168 ++++++++++-------- .../kanade/tachiyomi/network/NetworkHelper.kt | 19 +- .../tachiyomi/network/PersistentCookieJar.kt | 19 -- .../network/PersistentCookieStore.kt | 78 -------- .../ui/setting/SettingsAdvancedController.kt | 2 +- .../tachiyomi/util/WebViewClientCompat.kt | 83 +++++++++ 8 files changed, 250 insertions(+), 194 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index f871db2b2e..a83e8b9ffb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -7,9 +7,9 @@ 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.net.URI 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() { super.logout() preferences.trackToken(this).delete() - networkService.cookies.remove(URI(BASE_URL)) + networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!) } override val isLogged: Boolean get() = !getUsername().isEmpty() && !getPassword().isEmpty() && - checkCookies(URI(BASE_URL)) && + checkCookies() && !getCSRF().isEmpty() private fun getCSRF(): String = preferences.trackToken(this).getOrDefault() private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf) - private fun checkCookies(uri: URI): Boolean { + private fun checkCookies(): Boolean { var ckCount = 0 - - for (ck in networkService.cookies.get(uri)) { + 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++ } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt new file mode 100644 index 0000000000..8430b9cd00 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -0,0 +1,63 @@ +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) } + + override fun saveFromResponse(url: HttpUrl, cookies: MutableList) { + 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 { + return get(url) + } + + fun get(url: HttpUrl): List { + 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() + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 641cdd9602..9159cd9957 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -1,31 +1,32 @@ package eu.kanade.tachiyomi.network -import com.squareup.duktape.Duktape -import okhttp3.* +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import android.webkit.WebResourceResponse +import android.webkit.WebView +import eu.kanade.tachiyomi.util.WebViewClientCompat +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import timber.log.Timber import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit -class CloudflareInterceptor : 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+'([^']+)';""") +class CloudflareInterceptor(private val context: Context) : Interceptor { private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") - private interface IBase64 { - fun decode(input: String): String - } - - private val b64: IBase64 = object : IBase64 { - override fun decode(input: String): String { - return okio.ByteString.decodeBase64(input)!!.utf8() + private val handler by lazy { + val thread = HandlerThread("WebViewThread").apply { + uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e -> + Timber.e(e) + } + start() } + Handler(thread.looper) } @Synchronized @@ -34,8 +35,14 @@ class CloudflareInterceptor : Interceptor { // Check if Cloudflare anti-bot is on if (response.code() == 503 && response.header("Server") in serverCheck) { - return try { - chain.proceed(resolveChallenge(response)) + try { + response.close() + if (resolveWithWebView(chain.request())) { + // Retry original request + return chain.proceed(chain.request()) + } else { + throw Exception("Failed resolving Cloudflare challenge") + } } catch (e: Exception) { // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // we don't crash the entire app @@ -46,65 +53,76 @@ class CloudflareInterceptor : Interceptor { return response } - private fun resolveChallenge(response: Response): Request { - Duktape.create().use { duktape -> - val originalRequest = response.request() - val url = originalRequest.url() - val domain = url.host() - val content = response.body()!!.string() + private fun isChallengeResolverUrl(url: String): Boolean { + return "chk_jschl" in url + } - // CloudFlare requires waiting 4 seconds before resolving the challenge - Thread.sleep(4000) + @SuppressLint("SetJavaScriptEnabled") + private fun resolveWithWebView(request: Request): Boolean { + val latch = CountDownLatch(1) - val operation = operationPattern.find(content)?.groups?.get(1)?.value - val challenge = challengePattern.find(content)?.groups?.get(1)?.value - val pass = passPattern.find(content)?.groups?.get(1)?.value - val s = sPattern.find(content)?.groups?.get(1)?.value + var result = false + var isResolvingChallenge = false - // If `k` is null, it uses old methods. - val k = kPattern.find(content)?.groups?.get(1)?.value ?: "" - val innerHTMLValue = Regex("""(.*)""") - .find(content)?.groups?.get(3)?.value ?: "" + val requestUrl = request.url().toString() + val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" } - if (operation == null || challenge == null || pass == null || s == null) { - throw Exception("Failed resolving Cloudflare challenge") + handler.post { + val view = WebView(context) + view.settings.javaScriptEnabled = true + view.settings.userAgentString = request.header("User-Agent") + view.webViewClient = object : WebViewClientCompat() { + + override fun shouldInterceptRequestCompat( + view: WebView, + url: String + ): WebResourceResponse? { + val isChallengeResolverUrl = isChallengeResolverUrl(url) + if (requestUrl != url && !isChallengeResolverUrl) { + return WebResourceResponse("text/plain", "UTF-8", null) + } + + if (isChallengeResolverUrl) { + isResolvingChallenge = true + } + return null + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + if (isResolvingChallenge && url == requestUrl) { + setResultAndFinish(true) + } + } + + override fun onReceivedErrorCompat( + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String, + isMainFrame: Boolean + ) { + if ((errorCode != 503 && requestUrl == failingUrl) || + isChallengeResolverUrl(failingUrl) + ) { + setResultAndFinish(false) + } + } + + private fun setResultAndFinish(resolved: Boolean) { + result = resolved + latch.countDown() + view.stopLoading() + view.destroy() + } } - // 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()) + view.loadUrl(requestUrl, headers) } + + latch.await(12, TimeUnit.SECONDS) + + return result } - 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) }""" - } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 5e93894830..275dca17dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -2,11 +2,7 @@ package eu.kanade.tachiyomi.network import android.content.Context import android.os.Build -import okhttp3.Cache -import okhttp3.CipherSuite -import okhttp3.ConnectionSpec -import okhttp3.OkHttpClient -import okhttp3.TlsVersion +import okhttp3.* import java.io.File import java.io.IOException import java.net.InetAddress @@ -15,11 +11,7 @@ import java.net.UnknownHostException import java.security.KeyManagementException import java.security.KeyStore import java.security.NoSuchAlgorithmException -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocket -import javax.net.ssl.SSLSocketFactory -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager +import javax.net.ssl.* class NetworkHelper(context: Context) { @@ -27,7 +19,7 @@ class NetworkHelper(context: Context) { private val cacheSize = 5L * 1024 * 1024 // 5 MiB - private val cookieManager = PersistentCookieJar(context) + val cookieManager = AndroidCookieJar(context) val client = OkHttpClient.Builder() .cookieJar(cookieManager) @@ -36,12 +28,9 @@ class NetworkHelper(context: Context) { .build() val cloudflareClient = client.newBuilder() - .addInterceptor(CloudflareInterceptor()) + .addInterceptor(CloudflareInterceptor(context)) .build() - val cookies: PersistentCookieStore - get() = cookieManager.store - private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { return this diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt deleted file mode 100644 index fda9799789..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt +++ /dev/null @@ -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) { - store.addAll(url, cookies) - } - - override fun loadForRequest(url: HttpUrl): List { - return store.get(url) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt b/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt deleted file mode 100644 index ca854bb72f..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt +++ /dev/null @@ -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>() - 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 - 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) { - 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 { - return cookieMap[url].orEmpty().filter { !it.hasExpired() } - } - - private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt() - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 07f69eddee..3ad78247a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -43,7 +43,7 @@ class SettingsAdvancedController : SettingsController() { titleRes = R.string.pref_clear_cookies onClick { - network.cookies.removeAll() + network.cookieManager.removeAll() activity?.toast(R.string.cookies_cleared) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt b/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt new file mode 100644 index 0000000000..977dca5e6d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/WebViewClientCompat.kt @@ -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) + } + +} From ecc15201001a996bbba39173473f0f5046f8d2d3 Mon Sep 17 00:00:00 2001 From: inorichi Date: Tue, 2 Apr 2019 00:26:03 +0200 Subject: [PATCH 05/11] Use OkHttp to solve the challenge --- .../network/CloudflareInterceptor.kt | 116 +++++++++++------- 1 file changed, 73 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 9159cd9957..d955b9ae3d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.network import android.annotation.SuppressLint import android.content.Context +import android.os.Build import android.os.Handler -import android.os.HandlerThread +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 timber.log.Timber import java.io.IOException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -19,30 +20,33 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") - private val handler by lazy { - val thread = HandlerThread("WebViewThread").apply { - uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e -> - Timber.e(e) - } - start() + 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 } - Handler(thread.looper) } @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() - if (resolveWithWebView(chain.request())) { - // Retry original request - return chain.proceed(chain.request()) - } else { - throw Exception("Failed resolving Cloudflare challenge") - } + 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 @@ -53,45 +57,55 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { return response } - private fun isChallengeResolverUrl(url: String): Boolean { + private fun isChallengeSolutionUrl(url: String): Boolean { return "chk_jschl" in url } @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(request: Request): Boolean { + 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 result = false - var isResolvingChallenge = false + var webView: WebView? = null + var solutionUrl: String? = null + var challengeFound = false - val requestUrl = request.url().toString() + 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? { - val isChallengeResolverUrl = isChallengeResolverUrl(url) - if (requestUrl != url && !isChallengeResolverUrl) { + if (solutionUrl != null) { + // Intercept any request when we have the solution. return WebResourceResponse("text/plain", "UTF-8", null) } - - if (isChallengeResolverUrl) { - isResolvingChallenge = true - } return null } override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - if (isResolvingChallenge && url == requestUrl) { - setResultAndFinish(true) + if (url == origRequestUrl) { + // The first request didn't return the challenge, abort. + if (!challengeFound) { + latch.countDown() + } } } @@ -102,27 +116,43 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { failingUrl: String, isMainFrame: Boolean ) { - if ((errorCode != 503 && requestUrl == failingUrl) || - isChallengeResolverUrl(failingUrl) - ) { - setResultAndFinish(false) + if (isMainFrame) { + if (errorCode == 503) { + // Found the cloudflare challenge page. + challengeFound = true + } else { + // Unlock thread, the challenge wasn't found. + latch.countDown() + } + } + // Any error on the main frame that isn't the Cloudflare check should unlock + // OkHttp's thread. + if (errorCode != 503 && isMainFrame) { + latch.countDown() } } - - private fun setResultAndFinish(resolved: Boolean) { - result = resolved - latch.countDown() - view.stopLoading() - view.destroy() - } } - - view.loadUrl(requestUrl, headers) + 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) - return result + 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() } } From bf60aae9d887c81a16d0e4e6d622856e04c363c6 Mon Sep 17 00:00:00 2001 From: inorichi Date: Wed, 3 Apr 2019 09:47:07 +0200 Subject: [PATCH 06/11] Fix crashes below L --- .../kanade/tachiyomi/network/AndroidCookieJar.kt | 7 +++++++ .../tachiyomi/network/CloudflareInterceptor.kt | 14 +++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index 8430b9cd00..0795b5e5d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -14,6 +14,13 @@ class AndroidCookieJar(context: Context) : CookieJar { 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) { val urlString = url.toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index d955b9ae3d..a3f4283a11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -101,11 +101,12 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { } override fun onPageFinished(view: WebView, url: String) { - if (url == origRequestUrl) { + // 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. - if (!challengeFound) { - latch.countDown() - } + latch.countDown() } } @@ -125,11 +126,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { latch.countDown() } } - // Any error on the main frame that isn't the Cloudflare check should unlock - // OkHttp's thread. - if (errorCode != 503 && isMainFrame) { - latch.countDown() - } } } webView?.loadUrl(origRequestUrl, headers) From a62a7d533000dee0f83ca3ed2e2fa0826c8530e1 Mon Sep 17 00:00:00 2001 From: Pavka Date: Wed, 3 Apr 2019 11:14:37 +0300 Subject: [PATCH 07/11] Feature/shikomori track (#1905) * Add shikomori track * Fix char 'M' * Fix date in search --- app/src/main/AndroidManifest.xml | 15 ++ .../tachiyomi/data/track/TrackManager.kt | 6 +- .../tachiyomi/data/track/shikomori/OAuth.kt | 13 ++ .../data/track/shikomori/Shikomori.kt | 138 +++++++++++++ .../data/track/shikomori/ShikomoriApi.kt | 189 ++++++++++++++++++ .../track/shikomori/ShikomoriInterceptor.kt | 43 ++++ .../data/track/shikomori/ShikomoriModels.kt | 24 +++ .../ui/setting/SettingsTrackingController.kt | 11 + .../ui/setting/ShikomoriLoginActivity.kt | 50 +++++ .../main/res/drawable-xxxhdpi/shikomori.png | Bin 0 -> 8847 bytes 10 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/OAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/Shikomori.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriApi.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriInterceptor.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/ShikomoriLoginActivity.kt create mode 100644 app/src/main/res/drawable-xxxhdpi/shikomori.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c8f6aea26..ac0eb94e3c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,6 +52,21 @@ android:scheme="tachiyomi" /> + + + + + + + + + + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 73fa15c550..14558d1f1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -4,6 +4,7 @@ import android.content.Context import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist +import eu.kanade.tachiyomi.data.track.shikomori.Shikomori class TrackManager(private val context: Context) { @@ -11,6 +12,7 @@ class TrackManager(private val context: Context) { const val MYANIMELIST = 1 const val ANILIST = 2 const val KITSU = 3 + const val SHIKOMORI = 4 } val myAnimeList = Myanimelist(context, MYANIMELIST) @@ -19,7 +21,9 @@ class TrackManager(private val context: Context) { 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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/OAuth.kt new file mode 100644 index 0000000000..ad6adc18a4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/OAuth.kt @@ -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) +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/Shikomori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/Shikomori.kt new file mode 100644 index 0000000000..83fee74cf4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/Shikomori.kt @@ -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 { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override fun add(track: Track): Observable { + return api.addLibManga(track, getUsername()) + } + + override fun update(track: Track): Observable { + 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 { + 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> { + return api.search(query) + } + + override fun refresh(track: Track): Observable { + 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 { + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriApi.kt new file mode 100644 index 0000000000..2df1eae635 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriApi.kt @@ -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 { + 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 = addLibManga(track, user_id) + + fun search(search: String): Observable> { + 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 { + 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 { + 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()) + + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriInterceptor.kt new file mode 100644 index 0000000000..e46e7cfb4f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriInterceptor.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriModels.kt new file mode 100644 index 0000000000..d66f206495 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikomori/ShikomoriModels.kt @@ -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") +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index 699c253d23..250289cc1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService 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.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog @@ -53,6 +54,15 @@ class SettingsTrackingController : SettingsController(), 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) // Manually refresh anilist holder updatePreference(trackManager.aniList.id) + updatePreference(trackManager.shikomori.id) } private fun updatePreference(id: Int) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ShikomoriLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ShikomoriLoginActivity.kt new file mode 100644 index 0000000000..6c3ba6f839 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ShikomoriLoginActivity.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-xxxhdpi/shikomori.png b/app/src/main/res/drawable-xxxhdpi/shikomori.png new file mode 100644 index 0000000000000000000000000000000000000000..9859d16e6a7cb27d56117bf3b27bbd123c32c2ea GIT binary patch literal 8847 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 Date: Wed, 3 Apr 2019 04:22:32 -0400 Subject: [PATCH 08/11] add option to skip chapters marked read (#1791) --- .../data/preference/PreferenceKeys.kt | 2 + .../data/preference/PreferencesHelper.kt | 2 + .../tachiyomi/ui/reader/ReaderPresenter.kt | 285 +++++++++--------- .../ui/setting/SettingsReaderController.kt | 5 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 159 insertions(+), 136 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index a3d5297f66..1b2e6bb322 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -107,6 +107,8 @@ object PreferenceKeys { const val defaultCategory = "default_category" + const val skipRead = "skip_read" + const val downloadBadge = "display_download_badge" @Deprecated("Use the preferences of the source") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index c7b908f55e..58ad2b0aff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -167,6 +167,8 @@ class PreferencesHelper(val context: Context) { fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1) + fun skipRead() = prefs.getBoolean(Keys.skipRead, false) + fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE) fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index ab8b56ee83..14739759d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -32,7 +32,7 @@ import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import java.util.Date +import java.util.* import java.util.concurrent.TimeUnit /** @@ -84,12 +84,25 @@ class ReaderPresenter( private val chapterList by lazy { val manga = manga!! val dbChapters = db.getChapters(manga).executeAsBlocking() + 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) { - Manga.SORTING_SOURCE -> ChapterLoadBySource().get(dbChapters) - Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(dbChapters, selectedChapter) + Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader) + Manga.SORTING_NUMBER -> ChapterLoadByNumber().get(chaptersForReader, selectedChapter) else -> error("Unknown sorting method") }.map(::ReaderChapter) } @@ -165,12 +178,12 @@ class ReaderPresenter( if (!needsInit()) return db.getManga(mangaId).asRxObservable() - .first() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { init(it, initialChapterId) } - .subscribeFirst({ _, _ -> - // Ignore onNext event - }, ReaderActivity::setInitialChapterError) + .first() + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { init(it, initialChapterId) } + .subscribeFirst({ _, _ -> + // Ignore onNext event + }, ReaderActivity::setInitialChapterError) } /** @@ -193,13 +206,13 @@ class ReaderPresenter( // Read chapterList from an io thread because it's retrieved lazily and would block main. activeChapterSubscription?.unsubscribe() activeChapterSubscription = Observable - .fromCallable { chapterList.first { chapterId == it.chapter.id } } - .flatMap { getLoadObservable(loader!!, it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ _, _ -> - // Ignore onNext event - }, ReaderActivity::setInitialChapterError) + .fromCallable { chapterList.first { chapterId == it.chapter.id } } + .flatMap { getLoadObservable(loader!!, it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ _, _ -> + // Ignore onNext event + }, ReaderActivity::setInitialChapterError) } /** @@ -214,23 +227,23 @@ class ReaderPresenter( chapter: ReaderChapter ): Observable { return loader.loadChapter(chapter) - .andThen(Observable.fromCallable { - val chapterPos = chapterList.indexOf(chapter) + .andThen(Observable.fromCallable { + val chapterPos = chapterList.indexOf(chapter) - ViewerChapters(chapter, - chapterList.getOrNull(chapterPos - 1), - chapterList.getOrNull(chapterPos + 1)) - }) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { newChapters -> - val oldChapters = viewerChaptersRelay.value + ViewerChapters(chapter, + chapterList.getOrNull(chapterPos - 1), + chapterList.getOrNull(chapterPos + 1)) + }) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { newChapters -> + val oldChapters = viewerChaptersRelay.value - // Add new references first to avoid unnecessary recycling - newChapters.ref() - oldChapters?.unref() + // Add new references first to avoid unnecessary recycling + newChapters.ref() + oldChapters?.unref() - viewerChaptersRelay.call(newChapters) - } + viewerChaptersRelay.call(newChapters) + } } /** @@ -244,10 +257,10 @@ class ReaderPresenter( activeChapterSubscription?.unsubscribe() activeChapterSubscription = getLoadObservable(loader, chapter) - .toCompletable() - .onErrorComplete() - .subscribe() - .also(::add) + .toCompletable() + .onErrorComplete() + .subscribe() + .also(::add) } /** @@ -262,13 +275,13 @@ class ReaderPresenter( activeChapterSubscription?.unsubscribe() activeChapterSubscription = getLoadObservable(loader, chapter) - .doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) } - .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) } - .subscribeFirst({ view, _ -> - view.moveToPageIndex(0) - }, { _, _ -> - // Ignore onError event, viewers handle that state - }) + .doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) } + .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) } + .subscribeFirst({ view, _ -> + view.moveToPageIndex(0) + }, { _, _ -> + // Ignore onError event, viewers handle that state + }) } /** @@ -285,12 +298,12 @@ class ReaderPresenter( val loader = loader ?: return loader.loadChapter(chapter) - .observeOn(AndroidSchedulers.mainThread()) - // Update current chapters whenever a chapter is preloaded - .doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) } - .onErrorComplete() - .subscribe() - .also(::add) + .observeOn(AndroidSchedulers.mainThread()) + // Update current chapters whenever a chapter is preloaded + .doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) } + .onErrorComplete() + .subscribe() + .also(::add) } /** @@ -331,9 +344,9 @@ class ReaderPresenter( */ private fun saveChapterProgress(chapter: ReaderChapter) { db.updateChapterProgress(chapter.chapter).asRxCompletable() - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -342,9 +355,9 @@ class ReaderPresenter( private fun saveChapterHistory(chapter: ReaderChapter) { val history = History.create(chapter.chapter).apply { last_read = Date().time } db.updateHistoryLastRead(history).asRxCompletable() - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -394,18 +407,18 @@ class ReaderPresenter( db.updateMangaViewer(manga).executeAsBlocking() Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - val currChapters = viewerChaptersRelay.value - if (currChapters != null) { - // Save current page - val currChapter = currChapters.currChapter - currChapter.requestedPage = currChapter.chapter.last_page_read + .subscribeFirst({ view, _ -> + val currChapters = viewerChaptersRelay.value + if (currChapters != null) { + // Save current page + val currChapter = currChapters.currChapter + currChapter.requestedPage = currChapter.chapter.last_page_read - // Emit manga and chapters to the new viewer - view.setManga(manga) - view.setChapters(currChapters) - } - }) + // Emit manga and chapters to the new viewer + view.setManga(manga) + view.setChapters(currChapters) + } + }) } /** @@ -446,22 +459,22 @@ class ReaderPresenter( // Pictures directory. val destDir = File(Environment.getExternalStorageDirectory().absolutePath + - File.separator + Environment.DIRECTORY_PICTURES + - File.separator + "Tachiyomi") + File.separator + Environment.DIRECTORY_PICTURES + + File.separator + "Tachiyomi") // Copy file in background. Observable.fromCallable { saveImage(page, destDir, manga) } - .doOnNext { file -> - DiskUtil.scanMedia(context, file) - notifier.onComplete(file) - } - .doOnError { notifier.onError(it.message) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) }, - { view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) } - ) + .doOnNext { file -> + DiskUtil.scanMedia(context, file) + notifier.onComplete(file) + } + .doOnError { notifier.onError(it.message) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) }, + { view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) } + ) } /** @@ -479,13 +492,13 @@ class ReaderPresenter( val destDir = File(context.cacheDir, "shared_image") Observable.fromCallable { destDir.delete() } // Keep only the last shared file - .map { saveImage(page, destDir, manga) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.onShareImageResult(file) }, - { view, error -> /* Empty */ } - ) + .map { saveImage(page, destDir, manga) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, file -> view.onShareImageResult(file) }, + { view, error -> /* Empty */ } + ) } /** @@ -497,28 +510,28 @@ class ReaderPresenter( val stream = page.stream ?: return Observable - .fromCallable { - if (manga.source == LocalSource.ID) { - val context = Injekt.get() - LocalSource.updateCover(context, manga, stream()) - 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()) + .fromCallable { + if (manga.source == LocalSource.ID) { + val context = Injekt.get() + LocalSource.updateCover(context, manga, stream()) + R.string.cover_updated SetAsCoverResult.Success } 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()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, result -> view.onSetAsCoverResult(result) }, - { view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) } - ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, result -> view.onSetAsCoverResult(result) }, + { view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) } + ) } /** @@ -559,26 +572,26 @@ class ReaderPresenter( val trackManager = Injekt.get() db.getTracks(manga).asRxSingle() - .flatMapCompletable { trackList -> - Completable.concat(trackList.map { track -> - val service = trackManager.getService(track.sync_id) - if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) { - track.last_chapter_read = lastChapterRead + .flatMapCompletable { trackList -> + Completable.concat(trackList.map { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) { + track.last_chapter_read = lastChapterRead - // We wan't these to execute even if the presenter is destroyed and leaks - // for a while. The view can still be garbage collected. - Observable.defer { service.update(track) } - .map { db.insertTrack(track).executeAsBlocking() } - .toCompletable() - .onErrorComplete() - } else { - Completable.complete() - } - }) - } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + // We wan't these to execute even if the presenter is destroyed and leaks + // for a while. The view can still be garbage collected. + Observable.defer { service.update(track) } + .map { db.insertTrack(track).executeAsBlocking() } + .toCompletable() + .onErrorComplete() + } else { + Completable.complete() + } + }) + } + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -594,19 +607,19 @@ class ReaderPresenter( if (removeAfterReadSlots == -1) return Completable - .fromCallable { - // Position of the read chapter - val position = chapterList.indexOf(chapter) + .fromCallable { + // Position of the read chapter + val position = chapterList.indexOf(chapter) - // Retrieve chapter to delete according to preference - val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots) - if (chapterToDelete != null) { - downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga) + // Retrieve chapter to delete according to preference + val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots) + if (chapterToDelete != null) { + downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga) + } } - } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -615,9 +628,9 @@ class ReaderPresenter( */ private fun deletePendingChapters() { Completable.fromCallable { downloadManager.deletePendingChapters() } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index ac413f270e..ae59d13f19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -63,6 +63,11 @@ class SettingsReaderController : SettingsController() { defaultValue = "500" summary = "%s" } + switchPreference { + key = Keys.skipRead + titleRes = R.string.pref_skip_read_chapters + defaultValue = false + } switchPreference { key = Keys.fullscreen titleRes = R.string.pref_fullscreen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27befe0b91..f19427f625 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,6 +178,7 @@ Use custom brightness Use custom color filter Keep screen on + Skip chapters marked read Navigation Volume keys Invert volume keys From 8f2878a841223357d42fa3a97a6fda95a579ce95 Mon Sep 17 00:00:00 2001 From: Amine A <15179425+AmineI@users.noreply.github.com> Date: Wed, 3 Apr 2019 10:25:52 +0200 Subject: [PATCH 09/11] Added search intent handler and Google Search Action, for the global search (#1787) * Added search intent handler * Added support for Google Search actions --- app/src/main/AndroidManifest.xml | 7 ++++++- .../java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 12 ++++++++++++ app/src/main/res/xml/searchable.xml | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/xml/searchable.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac0eb94e3c..3e5f35a9b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,7 +28,12 @@ - + + + + + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d96b222c29..63d07429b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.main import android.animation.ObjectAnimator +import android.app.SearchManager import android.content.Intent import android.graphics.Color import android.os.Bundle @@ -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.controller.* import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.extension.ExtensionController import eu.kanade.tachiyomi.ui.library.LibraryController @@ -158,6 +160,16 @@ class MainActivity : BaseActivity() { 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 } return true diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml new file mode 100644 index 0000000000..f224a1c83c --- /dev/null +++ b/app/src/main/res/xml/searchable.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file From 8d4c0f505c15f071464bee52b6093c7de5e71981 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sun, 7 Apr 2019 14:58:40 +0200 Subject: [PATCH 10/11] Fix shared files not deleted from internal cache --- .../main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 14739759d6..fbb2f4bbfe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -491,7 +491,7 @@ class ReaderPresenter( 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) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) From e7606e6dca48f7a20c998398b976332a904106db Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 8 Apr 2019 02:08:40 +0200 Subject: [PATCH 11/11] Add option to open manga details in a WebView --- .../ui/manga/info/MangaInfoController.kt | 21 +++++--- .../ui/manga/info/MangaWebViewController.kt | 51 +++++++++++++++++++ .../res/layout/manga_info_web_controller.xml | 7 +++ app/src/main/res/menu/manga_info.xml | 6 ++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaWebViewController.kt create mode 100644 app/src/main/res/layout/manga_info_web_controller.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index cb9b091faf..f2a0f412ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -15,12 +15,7 @@ import android.support.customtabs.CustomTabsIntent import android.support.v4.content.pm.ShortcutInfoCompat import android.support.v4.content.pm.ShortcutManagerCompat import android.support.v4.graphics.drawable.IconCompat -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.Toast import com.afollestad.materialdialogs.MaterialDialog import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -138,6 +133,7 @@ class MangaInfoController : NucleusController(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_open_in_browser -> openInBrowser() + R.id.action_open_in_web_view -> openInWebView() R.id.action_share -> shareManga() R.id.action_add_to_home_screen -> addToHomeScreen() else -> return super.onOptionsItemSelected(item) @@ -302,6 +298,19 @@ class MangaInfoController : NucleusController(), } } + 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaWebViewController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaWebViewController.kt new file mode 100644 index 0000000000..15a85a1080 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaWebViewController.kt @@ -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() + + 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" + } + +} diff --git a/app/src/main/res/layout/manga_info_web_controller.xml b/app/src/main/res/layout/manga_info_web_controller.xml new file mode 100644 index 0000000000..6d52f5e22a --- /dev/null +++ b/app/src/main/res/layout/manga_info_web_controller.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/menu/manga_info.xml b/app/src/main/res/menu/manga_info.xml index 76f105d1da..6a5edad194 100644 --- a/app/src/main/res/menu/manga_info.xml +++ b/app/src/main/res/menu/manga_info.xml @@ -12,8 +12,12 @@ android:title="@string/action_open_in_browser" app:showAsAction="never"/> + + - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f19427f625..2388bdc1f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Resume Move Open in browser + Open in web view Add to home screen Change display mode Display