diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4407a8ccfb..7b6632b7dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,9 @@ android:name=".widget.CustomLayoutPickerActivity" android:label="@string/app_name" android:theme="@style/FilePickerTheme" /> + 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 10943d24a6..6f03090e6c 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 @@ -96,11 +96,11 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { return track } + suspend fun login(csrfToken: String) = login("myanimelist", csrfToken) + override suspend fun login(username: String, password: String): Boolean { - logout() return try { - val csrf = api.login(username, password) - saveCSRF(csrf) + saveCSRF(password) saveCredentials(username, password) true } catch (e: Exception) { @@ -110,28 +110,10 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { } } - suspend fun refreshLogin() { - val username = getUsername() - val password = getPassword() - logout() - - try { - val csrf = api.login(username, password) - saveCSRF(csrf) - saveCredentials(username, password) - } catch (e: Exception) { - Timber.e(e) - logout() - throw e - } - } - // Attempt to login again if cookies have been cleared but credentials are still filled suspend fun ensureLoggedIn() { if (isAuthorized) return if (!isLogged) throw Exception("MAL Login Credentials not found") - - refreshLogin() } override fun logout() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index f6621af6c7..21ad64b35b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -114,32 +114,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI } } - suspend fun login(username: String, password: String): String { - return withContext(Dispatchers.IO) { - val csrf = getSessionInfo() - login(username, password, csrf) - csrf - } - } - - private suspend fun getSessionInfo(): String { - val response = client.newCall(GET(loginUrl())).execute() - - return Jsoup.parse(response.consumeBody()).select("meta[name=csrf_token]").attr("content") - } - - private suspend fun login(username: String, password: String, csrf: String) { - withContext(Dispatchers.IO) { - val response = - client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf))) - .execute() - - response.use { - if (response.priorResponse?.code != 302) throw Exception("Authentication error") - } - } - } - private suspend fun getList(): List { val results = getListXml(getListUrl()).select("manga") @@ -174,7 +148,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI companion object { const val CSRF = "csrf_token" - private const val baseUrl = "https://myanimelist.net" + const val baseUrl = "https://myanimelist.net" private const val baseMangaUrl = "$baseUrl/manga/" private const val baseModifyListUrl = "$baseUrl/ownlist/manga" private const val PREFIX_MY = "my:" @@ -182,7 +156,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId - private fun loginUrl() = baseUrl.toUri().buildUpon().appendPath("login.php").toString() + fun loginUrl() = baseUrl.toUri().buildUpon().appendPath("login.php").build() private fun searchUrl(query: String): String { val col = "c[]" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 192f5ec672..03e3c6a95c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -21,17 +21,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor myanimelist.ensureLoggedIn() } val request = chain.request() - var response = chain.proceed(updateRequest(request)) - - if (response.code == 400) { - runBlocking { - myanimelist.refreshLogin() - } - response.close() - response = chain.proceed(updateRequest(request)) - } - - return response + return chain.proceed(updateRequest(request)) } private fun updateRequest(request: Request): Request { 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 cc0ad52415..cac8a04edf 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 @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi +import eu.kanade.tachiyomi.ui.setting.track.MyAnimeListLoginActivity import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog @@ -38,7 +39,13 @@ class SettingsTrackingController : trackPreference(trackManager.myAnimeList) { onClick { - showDialog(trackManager.myAnimeList) + if (trackManager.myAnimeList.isLogged) { + val dialog = TrackLogoutDialog(trackManager.myAnimeList) + dialog.targetController = this@SettingsTrackingController + dialog.showDialog(router) + } else { + startActivity(MyAnimeListLoginActivity.newIntent(context)) + } } } trackPreference(trackManager.aniList) { @@ -79,7 +86,7 @@ class SettingsTrackingController : override fun onActivityResumed(activity: Activity) { super.onActivityResumed(activity) - // Manually refresh anilist holder + updatePreference(trackManager.myAnimeList.id) updatePreference(trackManager.aniList.id) updatePreference(trackManager.shikimori.id) updatePreference(trackManager.bangumi.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt new file mode 100644 index 0000000000..bcec7b8b9a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.ui.setting.track + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.webkit.WebView +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.webview.BaseWebViewActivity +import eu.kanade.tachiyomi.util.system.WebViewClientCompat +import kotlinx.android.synthetic.main.webview_activity.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import uy.kohesive.injekt.injectLazy + +class MyAnimeListLoginActivity : BaseWebViewActivity() { + + private val trackManager: TrackManager by injectLazy() + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + title = "MyAnimeList" + + webview.webViewClient = object : WebViewClientCompat() { + override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { + view.loadUrl(url) + return true + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + // Get CSRF token from HTML after post-login redirect + if (url == MyAnimeListApi.baseUrl + "/") { + view?.evaluateJavascript( + "(function(){return document.querySelector('meta[name=csrf_token]').getAttribute('content')})();" + ) { + scope.launch { + withContext(Dispatchers.IO) { trackManager.myAnimeList.login(it.replace("\"", "")) } + returnToSettings() + } + } + } + } + } + webview.loadUrl(MyAnimeListApi.loginUrl().toString()) + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + + 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) + } + + companion object { + fun newIntent(context: Context): Intent { + val intent = Intent(context, MyAnimeListLoginActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + return intent + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt new file mode 100644 index 0000000000..4e80bee3f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt @@ -0,0 +1,255 @@ +package eu.kanade.tachiyomi.ui.webview + +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.TypedValue +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.graphics.ColorUtils +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.activity.BaseActivity +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.isInNightMode +import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.util.system.setDefaultSettings +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.invisible +import eu.kanade.tachiyomi.util.view.marginBottom +import eu.kanade.tachiyomi.util.view.setStyle +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.updatePadding +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.webview_activity.* + +open class BaseWebViewActivity : BaseActivity() { + + private var bundle: Bundle? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + delegate.localNightMode = when (preferences.theme()) { + 1, 8 -> AppCompatDelegate.MODE_NIGHT_NO + 2, 3, 4 -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + setContentView(R.layout.webview_activity) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + super.onBackPressed() + } + toolbar.navigationIcon?.setTint(getResourceColor(R.attr.actionBarTintColor)) + + val container: ViewGroup = findViewById(R.id.web_view_layout) + val content: LinearLayout = findViewById(R.id.web_linear_layout) + container.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + + container.setOnApplyWindowInsetsListener { v, insets -> + val contextView = window?.decorView?.findViewById(R.id.action_mode_bar) + contextView?.updateLayoutParams { + leftMargin = insets.systemWindowInsetLeft + rightMargin = insets.systemWindowInsetRight + } + // Consume any horizontal insets and pad all content in. There's not much we can do + // with horizontal insets + v.updatePadding( + left = insets.systemWindowInsetLeft, + right = insets.systemWindowInsetRight + ) + insets.replaceSystemWindowInsets( + 0, + insets.systemWindowInsetTop, + 0, + insets.systemWindowInsetBottom + ) + } + swipe_refresh.setStyle() + swipe_refresh.setOnRefreshListener { + refreshPage() + } + + window.statusBarColor = ColorUtils.setAlphaComponent( + getResourceColor( + R.attr + .colorSecondary + ), + 255 + ) + + content.setOnApplyWindowInsetsListener { v, insets -> + // if pure white theme on a device that does not support dark status bar + /*if (getResourceColor(android.R.attr.statusBarColor) != Color.TRANSPARENT) + window.statusBarColor = Color.BLACK + else window.statusBarColor = getResourceColor(R.attr.colorPrimary)*/ + window.navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val colorPrimary = getResourceColor(R.attr.colorPrimaryVariant) + if (colorPrimary == Color.WHITE) Color.BLACK + else getResourceColor(android.R.attr.colorPrimary) + } + // if the android q+ device has gesture nav, transparent nav bar + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + (v.rootWindowInsets.systemWindowInsetBottom != v.rootWindowInsets.tappableElementInsets.bottom) + ) { + getColor(android.R.color.transparent) + } else { + getResourceColor(android.R.attr.colorBackground) + } + v.setPadding( + insets.systemWindowInsetLeft, + insets.systemWindowInsetTop, + insets.systemWindowInsetRight, + 0 + ) + if (Build.VERSION.SDK_INT >= 26 && !isInNightMode()) { + content.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } + insets + } + + swipe_refresh.isEnabled = false + + if (bundle == null) { + webview.setDefaultSettings() + + webview.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + progressBar.visible() + progressBar.progress = newProgress + if (newProgress == 100) + progressBar.invisible() + super.onProgressChanged(view, newProgress) + } + } + val marginB = webview.marginBottom + webview.setOnApplyWindowInsetsListener { v, insets -> + val bottomInset = + if (Build.VERSION.SDK_INT >= 29) insets.tappableElementInsets.bottom + else insets.systemWindowInsetBottom + v.updateLayoutParams { + bottomMargin = marginB + bottomInset + } + insets + } + } else { + webview.restoreState(bundle) + } + } + + private fun refreshPage() { + swipe_refresh.isRefreshing = true + webview.reload() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val lightMode = !isInNightMode() + window.statusBarColor = ColorUtils.setAlphaComponent( + getResourceColor( + R.attr + .colorSecondary + ), + 255 + ) + toolbar.setBackgroundColor(getResourceColor(R.attr.colorSecondary)) + toolbar.popupTheme = if (lightMode) R.style.ThemeOverlay_MaterialComponents else R + .style.ThemeOverlay_MaterialComponents_Dark + val tintColor = getResourceColor(R.attr.actionBarTintColor) + toolbar.navigationIcon?.setTint(tintColor) + toolbar.overflowIcon?.mutate() + toolbar.setTitleTextColor(tintColor) + toolbar.overflowIcon?.setTint(tintColor) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + window.navigationBarColor = getResourceColor(R.attr.colorPrimaryVariant) + else if (window.navigationBarColor != getColor(android.R.color.transparent)) + window.navigationBarColor = getResourceColor(android.R.attr.colorBackground) + + web_linear_layout.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && lightMode) { + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility.or( + View + .SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + ) + } + val typedValue = TypedValue() + theme.resolveAttribute(android.R.attr.windowLightStatusBar, typedValue, true) + + if (typedValue.data == -1) + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility + .or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) + else + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility + .rem(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) + invalidateOptionsMenu() + } + + /** + * Called when the options menu of the toolbar is being created. It adds our custom menu. + */ + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.webview, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val backItem = toolbar.menu.findItem(R.id.action_web_back) + val forwardItem = toolbar.menu.findItem(R.id.action_web_forward) + backItem?.isEnabled = webview.canGoBack() + forwardItem?.isEnabled = webview.canGoForward() + val hasHistory = webview.canGoBack() || webview.canGoForward() + backItem?.isVisible = hasHistory + forwardItem?.isVisible = hasHistory + val tintColor = getResourceColor(R.attr.actionBarTintColor) + val translucentWhite = ColorUtils.setAlphaComponent(tintColor, 127) + backItem.icon?.setTint(if (webview.canGoBack()) tintColor else translucentWhite) + forwardItem?.icon?.setTint(if (webview.canGoForward()) tintColor else translucentWhite) + return super.onPrepareOptionsMenu(menu) + } + + override fun onBackPressed() { + if (webview.canGoBack()) webview.goBack() + else super.onBackPressed() + } + + /** + * Called when an item of the options menu was clicked. Used to handle clicks on our menu + * entries. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_web_back -> webview.goBack() + R.id.action_web_forward -> webview.goForward() + R.id.action_web_share -> shareWebpage() + R.id.action_web_browser -> openInBrowser() + } + return super.onOptionsItemSelected(item) + } + + private fun shareWebpage() { + try { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, webview.url) + } + startActivity(Intent.createChooser(intent, getString(R.string.share))) + } catch (e: Exception) { + toast(e.message) + } + } + + private fun openInBrowser() { + openInBrowser(webview.url) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index b75abe0f64..bdb5c9382a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -35,7 +35,7 @@ import eu.kanade.tachiyomi.util.view.visible import kotlinx.android.synthetic.main.webview_activity.* import uy.kohesive.injekt.injectLazy -class WebViewActivity : BaseActivity() { +open class WebViewActivity : BaseActivity() { private val sourceManager by injectLazy() private var bundle: Bundle? = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index 83e864a05a..eab28cf034 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -1,7 +1,20 @@ package eu.kanade.tachiyomi.util.system +import android.webkit.WebSettings import android.webkit.WebView +fun WebView.setDefaultSettings() { + with(settings) { + javaScriptEnabled = true + domStorageEnabled = true + databaseEnabled = true + setAppCacheEnabled(true) + useWideViewPort = true + loadWithOverviewMode = true + cacheMode = WebSettings.LOAD_DEFAULT + } +} + private val WEBVIEW_UA_VERSION_REGEX by lazy { Regex(""".*Chrome/(\d+)\..*""") }