Drop support for Android 4.x (#2440)

* Bump minSdkVersion

* Remove Android 4.x specific logic

* Consolidate res assets

* Add note about minimum Android version to README

* Restore incorrectly removed method, remove unneeded Lollipop TargetApi annotations
This commit is contained in:
arkon 2020-01-07 18:46:31 -05:00 committed by GitHub
parent b55814a1c0
commit 0d5099f230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 200 additions and 530 deletions

View File

@ -4,7 +4,7 @@
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi # ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android. Tachiyomi is a free and open source manga reader for Android 5.0 and above.
![screenshots of app](./.github/readme-images/screens.png) ![screenshots of app](./.github/readme-images/screens.png)

View File

@ -35,7 +35,7 @@ android {
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi" applicationId "eu.kanade.tachiyomi"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 41 versionCode 41

View File

@ -1,35 +1,20 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context
import android.os.Build
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.CookieSyncManager
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.CookieJar import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
class AndroidCookieJar(context: Context) : CookieJar { class AndroidCookieJar : CookieJar {
private val manager = CookieManager.getInstance() private val manager = CookieManager.getInstance()
private val syncManager by lazy { CookieSyncManager.createInstance(context) }
init {
// Init sync manager when using anything below L
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
syncManager
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString() val urlString = url.toString()
for (cookie in cookies) { for (cookie in cookies) {
manager.setCookie(urlString, cookie.toString()) manager.setCookie(urlString, cookie.toString())
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
syncManager.sync()
}
} }
override fun loadForRequest(url: HttpUrl): List<Cookie> { override fun loadForRequest(url: HttpUrl): List<Cookie> {
@ -39,7 +24,7 @@ class AndroidCookieJar(context: Context) : CookieJar {
fun get(url: HttpUrl): List<Cookie> { fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString()) val cookies = manager.getCookie(url.toString())
return if (cookies != null && !cookies.isEmpty()) { return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) } cookies.split(";").mapNotNull { Cookie.parse(url, it) }
} else { } else {
emptyList() emptyList()
@ -53,19 +38,10 @@ class AndroidCookieJar(context: Context) : CookieJar {
cookies.split(";") cookies.split(";")
.map { it.substringBefore("=") } .map { it.substringBefore("=") }
.onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") } .onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
syncManager.sync()
}
} }
fun removeAll() { fun removeAll() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { manager.removeAllCookies {}
manager.removeAllCookies {}
} else {
manager.removeAllCookie()
syncManager.sync()
}
} }
} }

View File

@ -28,11 +28,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
* Application class. * Application class.
*/ */
private val initWebView by lazy { private val initWebView by lazy {
if (Build.VERSION.SDK_INT >= 17) { WebSettings.getDefaultUserAgent(context)
WebSettings.getDefaultUserAgent(context)
} else {
null
}
} }
@Synchronized @Synchronized

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.base.holder package eu.kanade.tachiyomi.ui.base.holder
import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -51,10 +50,6 @@ interface SlicedHolder {
slice.showRightTopRect(topRect) slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect) slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect) slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
} }
@ -68,4 +63,4 @@ interface SlicedHolder {
val margin val margin
get() = 8.dpToPx get() = 8.dpToPx
} }

View File

@ -8,7 +8,6 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.view.animation.Animation import android.view.animation.Animation
@ -21,9 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.*
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
@ -276,10 +273,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() { toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
override fun onAnimationStart(animation: Animation) { override fun onAnimationStart(animation: Animation) {
// Fix status bar being translucent the first time it's opened. // Fix status bar being translucent the first time it's opened.
if (Build.VERSION.SDK_INT >= 21) { window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.addFlags(
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
} }
}) })
toolbar.startAnimation(toolbarAnimation) toolbar.startAnimation(toolbarAnimation)
@ -637,11 +631,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
*/ */
private fun setFullscreen(enabled: Boolean) { private fun setFullscreen(enabled: Boolean) {
systemUi = if (enabled) { systemUi = if (enabled) {
val level = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { val level = SystemUiHelper.LEVEL_IMMERSIVE
SystemUiHelper.LEVEL_IMMERSIVE
} else {
SystemUiHelper.LEVEL_HIDE_STATUS_BAR
}
val flags = SystemUiHelper.FLAG_IMMERSIVE_STICKY or val flags = SystemUiHelper.FLAG_IMMERSIVE_STICKY or
SystemUiHelper.FLAG_LAYOUT_IN_SCREEN_OLDER_DEVICES SystemUiHelper.FLAG_LAYOUT_IN_SCREEN_OLDER_DEVICES

View File

@ -3,16 +3,14 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.os.Build
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.AttributeSet import android.util.AttributeSet
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap
/** /**
@ -58,7 +56,6 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
} }
@TargetApi(Build.VERSION_CODES.KITKAT)
override fun onScrollStateChanged(state: Int) { override fun onScrollStateChanged(state: Int) {
super.onScrollStateChanged(state) super.onScrollStateChanged(state)
val layoutManager = layoutManager val layoutManager = layoutManager

View File

@ -5,10 +5,9 @@ import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceScreen
import android.view.View import android.view.View
import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -106,21 +105,12 @@ class SettingsBackupController : SettingsController() {
onClick { onClick {
val currentDir = preferences.backupsDirectory().getOrDefault() val currentDir = preferences.backupsDirectory().getOrDefault()
try{ try{
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
// Custom dir selected, open directory selector
preferences.context.getFilePicker(currentDir)
} else {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
}
startActivityForResult(intent, CODE_BACKUP_DIR) startActivityForResult(intent, CODE_BACKUP_DIR)
} catch (e: ActivityNotFoundException){ } catch (e: ActivityNotFoundException){
//Fall back to custom picker on error // Fall back to custom picker on error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
}
} }
} }
preferences.backupsDirectory().asObservable() preferences.backupsDirectory().asObservable()
@ -154,37 +144,27 @@ class SettingsBackupController : SettingsController() {
// Get uri of backup folder. // Get uri of backup folder.
val uri = data.data val uri = data.data
// Get UriPermission so it's possible to write files post kitkat. // Get UriPermission so it's possible to write files
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(uri, flags) activity.contentResolver.takePersistableUriPermission(uri, flags)
}
// Set backup Uri. // Set backup Uri
preferences.backupsDirectory().set(uri.toString()) preferences.backupsDirectory().set(uri.toString())
} }
CODE_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) { CODE_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
val activity = activity ?: return val activity = activity ?: return
val uri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val dir = data.data.path
val file = File(dir, Backup.getDefaultFilename())
Uri.fromFile(file) val uri = data.data
} else { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
val uri = data.data Intent.FLAG_GRANT_WRITE_URI_PERMISSION
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(uri, flags) activity.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(activity, uri) val file = UniFile.fromUri(activity, uri)
file.uri
}
CreatingBackupDialog().showDialog(router, TAG_CREATING_BACKUP_DIALOG) CreatingBackupDialog().showDialog(router, TAG_CREATING_BACKUP_DIALOG)
BackupCreateService.makeBackup(activity, uri, backupFlags) BackupCreateService.makeBackup(activity, file.uri, backupFlags)
} }
CODE_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) { CODE_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
val uri = data.data val uri = data.data
@ -201,25 +181,17 @@ class SettingsBackupController : SettingsController() {
val currentDir = preferences.backupsDirectory().getOrDefault() val currentDir = preferences.backupsDirectory().getOrDefault()
try { try {
// If API is lower than Lollipop use custom picker // Use Android's built-in file creator
val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
preferences.context.getFilePicker(currentDir)
} else {
// Use Androids build in file creator
Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE) .addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/*") .setType("application/*")
.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename()) .putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
}
startActivityForResult(intent, CODE_BACKUP_CREATE) startActivityForResult(intent, CODE_BACKUP_CREATE)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// Handle errors where the android ROM doesn't support the built in picker // Handle errors where the android ROM doesn't support the built in picker
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE)
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE)
}
} }
} }
class CreateBackupDialog : DialogController() { class CreateBackupDialog : DialogController() {

View File

@ -5,7 +5,6 @@ import android.app.Dialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -107,11 +106,7 @@ class SettingsDownloadController : SettingsController() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
DOWNLOAD_DIR_PRE_L -> if (data != null && resultCode == Activity.RESULT_OK) { DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
val uri = Uri.fromFile(File(data.data.path))
preferences.downloadsDirectory().set(uri.toString())
}
DOWNLOAD_DIR_L -> if (data != null && resultCode == Activity.RESULT_OK) {
val context = applicationContext ?: return val context = applicationContext ?: return
val uri = data.data val uri = data.data
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
@ -132,19 +127,11 @@ class SettingsDownloadController : SettingsController() {
} }
fun customDirectorySelected(currentDir: String) { fun customDirectorySelected(currentDir: String) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { try {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_PRE_L) startActivityForResult(intent, DOWNLOAD_DIR)
} else { } catch (e: ActivityNotFoundException) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR)
try {
startActivityForResult(intent, DOWNLOAD_DIR_L)
} catch (e: ActivityNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_L)
}
}
} }
} }
@ -183,7 +170,6 @@ class SettingsDownloadController : SettingsController() {
} }
private companion object { private companion object {
const val DOWNLOAD_DIR_PRE_L = 103 const val DOWNLOAD_DIR = 104
const val DOWNLOAD_DIR_L = 104
} }
} }

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.util
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.EnvironmentCompat import androidx.core.os.EnvironmentCompat
@ -45,13 +44,6 @@ object DiskUtil {
} }
} }
if (Build.VERSION.SDK_INT < 21) {
val extStorages = System.getenv("SECONDARY_STORAGE")
if (extStorages != null) {
directories += extStorages.split(":").map(::File)
}
}
return directories return directories
} }
@ -79,11 +71,7 @@ object DiskUtil {
* Scans the given file so that it can be shown in gallery apps, for example. * Scans the given file so that it can be shown in gallery apps, for example.
*/ */
fun scanMedia(context: Context, uri: Uri) { fun scanMedia(context: Context, uri: Uri) {
val action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
Intent.ACTION_MEDIA_MOUNTED
} else {
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
}
val mediaScanIntent = Intent(action) val mediaScanIntent = Intent(action)
mediaScanIntent.data = uri mediaScanIntent.data = uri
context.sendBroadcast(mediaScanIntent) context.sendBroadcast(mediaScanIntent)

View File

@ -36,7 +36,6 @@ abstract class WebViewClientCompat : WebViewClient() {
return shouldOverrideUrlCompat(view, url) return shouldOverrideUrlCompat(view, url)
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
final override fun shouldInterceptRequest( final override fun shouldInterceptRequest(
view: WebView, view: WebView,
request: WebResourceRequest request: WebResourceRequest

View File

@ -16,32 +16,26 @@ class ElevationAppBarLayout @JvmOverloads constructor(
private var origStateAnimator: StateListAnimator? = null private var origStateAnimator: StateListAnimator? = null
init { init {
if (Build.VERSION.SDK_INT >= 21) { origStateAnimator = stateListAnimator
origStateAnimator = stateListAnimator
}
} }
fun enableElevation() { fun enableElevation() {
if (Build.VERSION.SDK_INT >= 21) { stateListAnimator = origStateAnimator
stateListAnimator = origStateAnimator
}
} }
fun disableElevation() { fun disableElevation() {
if (Build.VERSION.SDK_INT >= 21) { stateListAnimator = StateListAnimator().apply {
stateListAnimator = StateListAnimator().apply { val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f)
val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f)
// Enabled and collapsible, but not collapsed means not elevated // Enabled and collapsible, but not collapsed means not elevated
addState(intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed), addState(intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed),
objAnimator) objAnimator)
// Default enabled state // Default enabled state
addState(intArrayOf(android.R.attr.enabled), objAnimator) addState(intArrayOf(android.R.attr.enabled), objAnimator)
// Disabled state // Disabled state
addState(IntArray(0), objAnimator) addState(IntArray(0), objAnimator)
}
} }
} }

View File

@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.widget
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
View(context, attrs) { View(context, attrs) {
@ -21,28 +18,25 @@ class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: Att
* @param initialRadius size of radius of animation * @param initialRadius size of radius of animation
*/ */
fun hideRevealEffect(centerX: Int, centerY: Int, initialRadius: Int) { fun hideRevealEffect(centerX: Int, centerY: Int, initialRadius: Int) {
if (Build.VERSION.SDK_INT >= 21) { // Make the view visible.
this.visibility = View.VISIBLE
// Make the view visible. // Create the animation (the final radius is zero).
this.visibility = View.VISIBLE val anim = ViewAnimationUtils.createCircularReveal(
this, centerX, centerY, initialRadius.toFloat(), 0f)
// Create the animation (the final radius is zero). // Set duration of animation.
val anim = ViewAnimationUtils.createCircularReveal( anim.duration = 500
this, centerX, centerY, initialRadius.toFloat(), 0f)
// Set duration of animation. // make the view invisible when the animation is done
anim.duration = 500 anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
this@RevealAnimationView.visibility = View.INVISIBLE
}
})
// make the view invisible when the animation is done anim.start()
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
this@RevealAnimationView.visibility = View.INVISIBLE
}
})
anim.start()
}
} }
/** /**
@ -55,25 +49,20 @@ class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: Att
* @return sdk version lower then 21 * @return sdk version lower then 21
*/ */
fun showRevealEffect(centerX: Int, centerY: Int, listener: Animator.AnimatorListener): Boolean { fun showRevealEffect(centerX: Int, centerY: Int, listener: Animator.AnimatorListener): Boolean {
if (Build.VERSION.SDK_INT >= 21) { this.visibility = View.VISIBLE
this.visibility = View.VISIBLE val height = this.height
val height = this.height // Create animation
val anim = ViewAnimationUtils.createCircularReveal(
this, centerX, centerY, 0f, height.toFloat())
// Create animation // Set duration of animation
val anim = ViewAnimationUtils.createCircularReveal( anim.duration = 350
this, centerX, centerY, 0f, height.toFloat())
// Set duration of animation anim.addListener(listener)
anim.duration = 350 anim.start()
return true
anim.addListener(listener)
anim.start()
return true
}
return false
} }
} }

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selectorColorDark"
>
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorDark"/>
</item>
<item android:state_activated="true">
<color android:color="@color/selectorColorDark"/>
</item>
<item>
<color android:color="@color/md_black_1000"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorAccentDark"
>
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorDark"/>
</item>
<item android:state_activated="true">
<color android:color="@color/selectorColorDark"/>
</item>
<item>
<color android:color="@color/backgroundDark"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorAccentLight"
>
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorLight"/>
</item>
<item android:state_activated="true">
<color android:color="@color/selectorColorLight"/>
</item>
<item>
<color android:color="@color/backgroundLight"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/rippleColorDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorDark"/>
</item>
<item android:state_activated="true">
<color android:color="@color/rippleColorDark"/>
</item>
<item>
<color android:color="@color/md_black_1000"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/rippleColorDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorDark"/>
</item>
<item android:state_activated="true">
<color android:color="@color/rippleColorDark"/>
</item>
<item>
<color android:color="@color/dialogDark"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/rippleColorLight">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorLight"/>
</item>
<item android:state_activated="true">
<color android:color="@color/rippleColorLight"/>
</item>
<item>
<color android:color="@color/dialogLight"/>
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector android:exitFadeDuration="@android:integer/config_longAnimTime" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> android:color="@color/selectorColorDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorDark" />
</item>
<item android:state_focused="true" android:drawable="@color/selectorColorDark"/> <item android:state_activated="true">
<item android:state_pressed="true" android:drawable="@color/selectorColorDark"/> <color android:color="@color/selectorColorDark" />
<item android:state_activated="true" android:drawable="@color/selectorColorDark"/> </item>
<item android:drawable="@color/md_black_1000"/>
</selector> <item>
<color android:color="@color/md_black_1000" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector android:exitFadeDuration="@android:integer/config_longAnimTime" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> android:color="@color/colorAccentDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorDark" />
</item>
<item android:state_focused="true" android:drawable="@color/selectorColorDark"/> <item android:state_activated="true">
<item android:state_pressed="true" android:drawable="@color/selectorColorDark"/> <color android:color="@color/selectorColorDark" />
<item android:state_activated="true" android:drawable="@color/selectorColorDark"/> </item>
<item android:drawable="@color/backgroundDark"/>
</selector> <item>
<color android:color="@color/backgroundDark" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector android:exitFadeDuration="@android:integer/config_longAnimTime" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> android:color="@color/colorAccentLight">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/selectorColorLight" />
</item>
<item android:state_focused="true" android:drawable="@color/selectorColorLight"/> <item android:state_activated="true">
<item android:state_pressed="true" android:drawable="@color/selectorColorLight"/> <color android:color="@color/selectorColorLight" />
<item android:state_activated="true" android:drawable="@color/selectorColorLight"/> </item>
<item android:drawable="@color/backgroundLight"/>
</selector> <item>
<color android:color="@color/backgroundLight" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_longAnimTime"> android:color="@color/rippleColorDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorDark" />
</item>
<item android:drawable="@color/rippleColorDark" android:state_focused="true"/> <item android:state_activated="true">
<item android:drawable="@color/rippleColorDark" android:state_pressed="true"/> <color android:color="@color/rippleColorDark" />
<item android:drawable="@color/rippleColorDark" android:state_activated="true"/> </item>
<item android:drawable="@color/md_black_1000"/>
</selector> <item>
<color android:color="@color/md_black_1000" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_longAnimTime"> android:color="@color/rippleColorDark">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorDark" />
</item>
<item android:drawable="@color/rippleColorDark" android:state_focused="true"/> <item android:state_activated="true">
<item android:drawable="@color/rippleColorDark" android:state_pressed="true"/> <color android:color="@color/rippleColorDark" />
<item android:drawable="@color/rippleColorDark" android:state_activated="true"/> </item>
<item android:drawable="@color/dialogDark"/>
</selector> <item>
<color android:color="@color/dialogDark" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_longAnimTime"> android:color="@color/rippleColorLight">
<item>
<selector>
<item android:state_selected="true">
<color android:color="@color/rippleColorLight" />
</item>
<item android:drawable="@color/rippleColorLight" android:state_focused="true"/> <item android:state_activated="true">
<item android:drawable="@color/rippleColorLight" android:state_pressed="true"/> <color android:color="@color/rippleColorLight" />
<item android:drawable="@color/rippleColorLight" android:state_activated="true"/> </item>
<item android:drawable="@color/dialogLight"/>
</selector> <item>
<color android:color="@color/dialogLight" />
</item>
</selector>
</item>
</ripple>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Nav header-->
<dimen name="navigation_drawer_header_margin">41dp</dimen>
</resources>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- String Fonts -->
<string name="font_roboto_medium" translatable="false">sans-serif-medium</string>
<string name="font_roboto_regular" translatable="false">sans-serif-regular</string>
</resources>

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--===========-->
<!-- Main Theme-->
<!--===========-->
<style name="Theme.Tachiyomi" parent="Theme.Base">
<!-- Attributes specific for SDK 21 and up -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
<!--=============-->
<!-- Dark Themes -->
<!--=============-->
<style name="Theme.Tachiyomi.Dark" parent="Theme.Base.Dark">
<!-- Attributes specific for SDK 21 and up -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorDarkPrimaryDark</item>
</style>
<style name="Theme.Tachiyomi.DarkBlue" parent="Theme.Base.Dark">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<!-- Attributes specific for SDK 21 and up -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorDarkPrimaryDark</item>
</style>
<!--==============-->
<!-- Amoled Theme -->
<!--==============-->
<style name="Theme.Tachiyomi.Amoled" parent="Theme.Base.Amoled">
<!-- Attributes specific for SDK 21 and up -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<!--==============-->
<!-- Reader Theme -->
<!--==============-->
<style name="Theme.Reader" parent="Theme.Base.Reader.Dark">
<!-- Attributes specific for SDK 21 and up -->
<item name="android:statusBarColor">?colorPrimaryDark</item>
<item name="android:navigationBarColor">?colorPrimaryDark</item>
</style>
<style name="Theme.Reader.Light" parent="Theme.Base.Reader.Light">
<!-- Attributes specific for SDK 21 and up -->
<item name="android:statusBarColor">?colorPrimaryDark</item>
<item name="android:navigationBarColor">?colorPrimaryDark</item>
</style>
</resources>

View File

@ -20,10 +20,9 @@
<dimen name="text_body">16sp</dimen> <dimen name="text_body">16sp</dimen>
<dimen name="text_small_body">14sp</dimen> <dimen name="text_small_body">14sp</dimen>
<!--Nav header--> <!--Nav header-->
<dimen name="navigation_drawer_header_height">158dp</dimen> <dimen name="navigation_drawer_header_height">158dp</dimen>
<dimen name="navigation_drawer_header_margin">16dp</dimen> <dimen name="navigation_drawer_header_margin">41dp</dimen>
<dimen name="bottom_sheet_width">0dp</dimen> <dimen name="bottom_sheet_width">0dp</dimen>
</resources> </resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- String Fonts -->
<string name="font_roboto_medium" translatable="false">sans-serif</string>
<string name="font_roboto_regular" translatable="false">sans-serif</string>
</resources>

View File

@ -50,7 +50,7 @@
</style> </style>
<style name="TextAppearance.Regular"> <style name="TextAppearance.Regular">
<item name="android:fontFamily">@string/font_roboto_regular</item> <item name="android:fontFamily">sans-serif-regular</item>
</style> </style>
<style name="TextAppearance.Regular.Body1"> <style name="TextAppearance.Regular.Body1">
@ -102,7 +102,7 @@
</style> </style>
<style name="TextAppearance.Medium"> <style name="TextAppearance.Medium">
<item name="android:fontFamily">@string/font_roboto_medium</item> <item name="android:fontFamily">sans-serif-medium</item>
</style> </style>
<style name="TextAppearance.Medium.Title"> <style name="TextAppearance.Medium.Title">

View File

@ -39,8 +39,13 @@
<item name="icon_color">@color/iconColorLight</item> <item name="icon_color">@color/iconColorLight</item>
</style> </style>
<!--===========-->
<!-- Main Theme-->
<!--===========-->
<style name="Theme.Tachiyomi" parent="Theme.Base"> <style name="Theme.Tachiyomi" parent="Theme.Base">
<!-- Attributes specific for SDK 16 to SDK 20 --> <item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style> </style>
<!--=============--> <!--=============-->
@ -61,6 +66,10 @@
<item name="android:divider">@color/dividerDark</item> <item name="android:divider">@color/dividerDark</item>
<item name="android:listDivider">@drawable/line_divider_dark</item> <item name="android:listDivider">@drawable/line_divider_dark</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorDarkPrimaryDark</item>
<!-- Themes --> <!-- Themes -->
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item> <item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
@ -86,6 +95,10 @@
<style name="Theme.Tachiyomi.DarkBlue" parent="Theme.Base.Dark"> <style name="Theme.Tachiyomi.DarkBlue" parent="Theme.Base.Dark">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorDarkPrimaryDark</item>
</style> </style>
<!--==============--> <!--==============-->
@ -96,6 +109,10 @@
<item name="colorPrimaryDark">@color/colorAmoledPrimary</item> <item name="colorPrimaryDark">@color/colorAmoledPrimary</item>
<item name="android:colorBackground">@color/md_black_1000</item> <item name="android:colorBackground">@color/md_black_1000</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- Custom Attributes--> <!-- Custom Attributes-->
<item name="selectable_list_drawable">@drawable/list_item_selector_amoled</item> <item name="selectable_list_drawable">@drawable/list_item_selector_amoled</item>
<item name="selectable_library_drawable">@drawable/library_item_selector_amoled</item> <item name="selectable_library_drawable">@drawable/library_item_selector_amoled</item>
@ -113,12 +130,18 @@
<item name="colorPrimary">@color/colorDarkPrimary</item> <item name="colorPrimary">@color/colorDarkPrimary</item>
<item name="colorPrimaryDark">@color/colorDarkPrimaryDark</item> <item name="colorPrimaryDark">@color/colorDarkPrimaryDark</item>
<item name="android:colorBackground">@android:color/black</item> <item name="android:colorBackground">@android:color/black</item>
<item name="android:statusBarColor">?colorPrimaryDark</item>
<item name="android:navigationBarColor">?colorPrimaryDark</item>
</style> </style>
<style name="Theme.Base.Reader.Light" parent="Theme.Base"> <style name="Theme.Base.Reader.Light" parent="Theme.Base">
<item name="colorPrimary">@color/colorDarkPrimary</item> <item name="colorPrimary">@color/colorDarkPrimary</item>
<item name="colorPrimaryDark">@color/colorDarkPrimaryDark</item> <item name="colorPrimaryDark">@color/colorDarkPrimaryDark</item>
<item name="android:colorBackground">@android:color/white</item> <item name="android:colorBackground">@android:color/white</item>
<item name="android:statusBarColor">?colorPrimaryDark</item>
<item name="android:navigationBarColor">?colorPrimaryDark</item>
</style> </style>
<style name="Theme.Reader" parent="Theme.Base.Reader.Dark"> <style name="Theme.Reader" parent="Theme.Base.Reader.Dark">