aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt42
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt18
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt9
14 files changed, 84 insertions, 57 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
index 267a07a2..08728ae4 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
@@ -6,6 +6,9 @@ import ca.allanwang.kau.internal.KauBaseActivity
import ca.allanwang.kau.searchview.SearchViewHolder
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.utils.setFrostTheme
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
+import io.reactivex.rxkotlin.addTo
/**
* Created by Allan Wang on 2017-06-12.
@@ -17,6 +20,8 @@ abstract class BaseActivity : KauBaseActivity() {
*/
protected open fun backConsumer(): Boolean = false
+ private val compositeDisposable = CompositeDisposable()
+
final override fun onBackPressed() {
if (this is SearchViewHolder && searchViewOnBackPress()) return
if (this is VideoViewHolder && videoOnBackPress()) return
@@ -29,7 +34,16 @@ abstract class BaseActivity : KauBaseActivity() {
if (this !is WebOverlayActivityBase) setFrostTheme()
}
-//
+ override fun onDestroy() {
+ compositeDisposable.dispose()
+ super.onDestroy()
+ }
+
+ fun Disposable.disposeOnDestroy() {
+ compositeDisposable.add(this)
+ }
+
+ //
// private var networkDisposable: Disposable? = null
// private var networkConsumer: ((Connectivity) -> Unit)? = null
//
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
index 0cd7dacd..db49d994 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -138,15 +138,15 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
shouldShow = false
fab.backgroundTintList = ColorStateList.valueOf(Prefs.headerColor.withMinAlpha(200))
fab.hide()
- appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
- if (!hasFab) return@addOnOffsetChangedListener
+ appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
+ if (!hasFab) return@OnOffsetChangedListener
val percent = Math.abs(verticalOffset.toFloat() / appBarLayout.totalScrollRange)
val shouldShow = percent < 0.2
if (this.shouldShow != shouldShow) {
this.shouldShow = shouldShow
fab.showIf(shouldShow)
}
- }
+ })
}
override fun showFab(iicon: IIcon, clickEvent: () -> Unit) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
index dd0649b3..b5e2119f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -103,7 +103,7 @@ class LoginActivity : BaseActivity() {
launchNewTask<MainActivity>(cookies, true)
}, 1000)
}
- }
+ }.disposeOnDestroy()
loadProfile(cookie.id)
loadUsername(cookie)
}
@@ -126,7 +126,7 @@ class LoginActivity : BaseActivity() {
}
private fun loadUsername(cookie: CookieModel) {
- cookie.fetchUsername(usernameSubject::onSuccess)
+ cookie.fetchUsername(usernameSubject::onSuccess).disposeOnDestroy()
}
override fun backConsumer(): Boolean {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index d153b5d9..77ca37f3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -6,6 +6,8 @@ import android.support.v4.view.ViewPager
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.views.BadgedIcon
import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.rxkotlin.addTo
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import org.jsoup.Jsoup
@@ -83,7 +85,7 @@ class MainActivity : BaseMainActivity() {
FbItem.NOTIFICATIONS.icon -> view.badgeText = notifications
}
}
- }
+ }.disposeOnDestroy()
adapter.pages.forEach {
tabs.addTab(tabs.newTab()
.setCustomView(BadgedIcon(this).apply { iicon = it.icon }))
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
index ec8f11ff..3081c463 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
@@ -158,6 +158,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
content.titleObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { toolbar.title = it }
+ .disposeOnDestroy()
with(web) {
if (forceBasicAgent) //todo check; the webview already adds it dynamically
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
index 69cf32b7..1fe65d5a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
@@ -13,6 +13,7 @@ import com.raizlabs.android.dbflow.annotation.PrimaryKey
import com.raizlabs.android.dbflow.annotation.Table
import com.raizlabs.android.dbflow.kotlinextensions.*
import com.raizlabs.android.dbflow.structure.BaseModel
+import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import paperparcel.PaperParcel
import java.net.UnknownHostException
@@ -67,26 +68,25 @@ fun removeCookie(id: Long) {
}
}
-inline fun CookieModel.fetchUsername(crossinline callback: (String) -> Unit) {
- ReactiveNetwork.checkInternetConnectivity().subscribeOn(Schedulers.io()).subscribe { yes, _ ->
- if (!yes) return@subscribe callback("")
- var result = ""
- try {
- result = frostJsoup(cookie, FbItem.PROFILE.url).title()
- L.d { "Fetch username found" }
- } catch (e: Exception) {
- if (e !is UnknownHostException)
- e.logFrostEvent("Fetch username failed")
- } finally {
- if (result.isBlank() && (name?.isNotBlank() == true)) {
- callback(name!!)
- return@subscribe
+inline fun CookieModel.fetchUsername(crossinline callback: (String) -> Unit): Disposable =
+ ReactiveNetwork.checkInternetConnectivity().subscribeOn(Schedulers.io()).subscribe { yes, _ ->
+ if (!yes) return@subscribe callback("")
+ var result = ""
+ try {
+ result = frostJsoup(cookie, FbItem.PROFILE.url).title()
+ L.d { "Fetch username found" }
+ } catch (e: Exception) {
+ if (e !is UnknownHostException)
+ e.logFrostEvent("Fetch username failed")
+ } finally {
+ if (result.isBlank() && (name?.isNotBlank() == true)) {
+ callback(name!!)
+ return@subscribe
+ }
+ if (name != result) {
+ name = result
+ saveFbCookie(this@fetchUsername)
+ }
+ callback(result)
}
- if (name != result) {
- name = result
- saveFbCookie(this@fetchUsername)
- }
- callback(result)
}
- }
-} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
index 95162932..f5f7463d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
@@ -10,6 +10,8 @@ import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.utils.unescapeHtml
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.rxkotlin.addTo
import okhttp3.Request
import okhttp3.ResponseBody
import org.jsoup.Jsoup
@@ -71,6 +73,8 @@ class OfflineWebsite(private val url: String,
.get()
.call()
+ private val compositeDisposable = CompositeDisposable()
+
/**
* Caller to bind callbacks and start the load
* Callback is guaranteed to be called unless the load is cancelled
@@ -155,7 +159,7 @@ class OfflineWebsite(private val url: String,
progress(100)
callback(true)
}
- }
+ }.addTo(compositeDisposable)
}
fun zip(name: String): Boolean {
@@ -217,7 +221,7 @@ class OfflineWebsite(private val url: String,
})
private fun downloadCss() = cssQueue.clean().toTypedArray().zip<String, Set<String>, Set<String>>({
- it.flatMap { it }.toSet()
+ it.flatMap { l -> l }.toSet()
}, {
it.downloadUrl({ emptySet() }) { file, body ->
var content = body.string()
@@ -319,6 +323,7 @@ class OfflineWebsite(private val url: String,
fun cancel() {
cancelled = true
+ compositeDisposable.dispose()
L.v { "Request cancelled" }
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
index a02cbe59..d6915f75 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
@@ -39,7 +39,7 @@ enum class FbItem(
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
- _SEARCH(R.string.search_menu_title, GoogleMaterial.Icon.gmd_search, "search/top"),
+ _SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
SETTINGS(R.string.settings, GoogleMaterial.Icon.gmd_settings, "settings"),
;
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
index 1698ae13..e45e86b1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
@@ -5,6 +5,7 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.web.FrostWebViewClient
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
import io.reactivex.subjects.SingleSubject
import org.apache.commons.text.StringEscapeUtils
import java.util.*
@@ -76,28 +77,29 @@ interface InjectorContract {
/**
* Helper method to inject multiple functions simultaneously with a single callback
*/
-fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Int) -> Unit)? = null) {
+fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Int) -> Unit)? = null): Disposable? {
val validInjectors = injectors.filter { it != JsActions.EMPTY }
if (validInjectors.isEmpty()) {
callback?.invoke(0)
- return
+ return null
}
L.d { "Injecting ${validInjectors.size} items" }
if (callback == null) {
validInjectors.forEach { it.inject(this) }
- return
+ return null
}
- val observables = Array(validInjectors.size, { SingleSubject.create<Unit>() })
- Single.zip<Unit, Int>(observables.asList(), { it.size })
+ val observables = Array(validInjectors.size) { SingleSubject.create<Unit>() }
+ val disposable = Single.zip<Unit, Int>(observables.asList()) { it.size }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { res, _ ->
callback(res)
}
(0 until validInjectors.size).forEach { i ->
- validInjectors[i].inject(this, {
+ validInjectors[i].inject(this) {
observables[i].onSuccess(Unit)
- })
+ }
}
+ return disposable
}
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract,
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
index a89fdc54..dbd4d0d1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -3,7 +3,6 @@ package com.pitchedapps.frost.utils
import android.graphics.Color
import ca.allanwang.kau.kotlin.lazyResettable
import ca.allanwang.kau.kpref.KPref
-import ca.allanwang.kau.kpref.StringSet
import ca.allanwang.kau.kpref.kpref
import ca.allanwang.kau.utils.colorToForeground
import ca.allanwang.kau.utils.isColorVisibleOn
@@ -111,7 +110,7 @@ object Prefs : KPref() {
var animate: Boolean by kpref("fancy_animations", true)
- var notificationKeywords: StringSet by kpref("notification_keywords", mutableSetOf())
+ var notificationKeywords: Set<String> by kpref("notification_keywords", mutableSetOf())
var notificationsGeneral: Boolean by kpref("notification_general", true)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
index b4b78bf2..66ef9723 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -9,8 +9,8 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.support.annotation.StringRes
-import android.support.design.internal.SnackbarContentLayout
import android.support.design.widget.Snackbar
+import android.support.design.widget.SnackbarContentLayout
import android.support.v4.content.FileProvider
import android.support.v7.widget.Toolbar
import android.view.View
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
index 4622971b..19b1176e 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
@@ -17,7 +17,9 @@ import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
+import io.reactivex.rxkotlin.addTo
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
@@ -53,6 +55,8 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
override val refreshObservable: PublishSubject<Boolean> = PublishSubject.create()
override val titleObservable: BehaviorSubject<String> = BehaviorSubject.create()
+ private val compositeDisposable = CompositeDisposable()
+
override lateinit var baseUrl: String
override var baseEnum: FbItem? = null
@@ -81,14 +85,14 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
progress.setProgress(it, true)
else
progress.progress = it
- }
+ }.addTo(compositeDisposable)
refreshObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
refresh.isRefreshing = it
refresh.isEnabled = true
- }
+ }.addTo(compositeDisposable)
refresh.setOnRefreshListener { coreView.reload(true) }
reloadThemeSelf()
@@ -126,6 +130,7 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
progressObservable.onComplete()
refreshObservable.onComplete()
core.destroy()
+ compositeDisposable.dispose()
}
private var dispose: Disposable? = null
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
index c45bf23d..d7f44420 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
@@ -14,6 +14,7 @@ import ca.allanwang.kau.utils.AnimHolder
import ca.allanwang.kau.utils.dpToPx
import ca.allanwang.kau.utils.scaleXY
import ca.allanwang.kau.utils.toast
+import com.devbrackets.android.exomedia.ui.widget.VideoControls
import com.devbrackets.android.exomedia.ui.widget.VideoView
import com.pitchedapps.frost.R
import com.pitchedapps.frost.utils.L
@@ -78,7 +79,7 @@ class FrostVideoView @JvmOverloads constructor(
if (!isPlaying) showControls()
else viewerContract.onControlsHidden()
}
- }
+ }.start()
} else {
hideControls()
val (scale, tX, tY) = mapBounds()
@@ -89,7 +90,7 @@ class FrostVideoView @JvmOverloads constructor(
withAnimator(origScale, scale) { scaleXY = it }
withAnimator(origX, tX) { translationX = it }
withAnimator(origY, tY) { translationY = it }
- }
+ }.start()
}
}
@@ -144,7 +145,8 @@ class FrostVideoView @JvmOverloads constructor(
}
setOnTouchListener(FrameTouchListener(context))
v.setOnTouchListener(VideoTouchListener(context))
- setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight ->
+ setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight, pixelWidthHeightRatio ->
+ // todo use provided ratio?
val ratio = Math.min(width.toFloat() / intrinsicWidth, height.toFloat() / intrinsicHeight.toFloat())
/**
* Only remap if not expanded and if dimensions have changed
@@ -158,7 +160,7 @@ class FrostVideoView @JvmOverloads constructor(
fun setViewerContract(contract: FrostVideoViewerContract) {
this.viewerContract = contract
- videoControls?.setVisibilityListener(viewerContract)
+ (videoControls as? VideoControls)?.setVisibilityListener(viewerContract)
}
fun jumpToStart() {
@@ -186,7 +188,7 @@ class FrostVideoView @JvmOverloads constructor(
private fun hideControls() {
if (videoControls?.isVisible == true)
- videoControls?.hide()
+ videoControls?.hide(false)
}
private fun toggleControls() {
@@ -204,11 +206,11 @@ class FrostVideoView @JvmOverloads constructor(
fun destroy() {
stopPlayback()
if (alpha > 0f)
- ProgressAnimator.ofFloat(alpha, 0f) {
+ ProgressAnimator.ofFloat {
duration = FAST_ANIMATION_DURATION
- withAnimator { alpha = it }
+ withAnimator(alpha, 0f) { alpha = it }
withEndAction { onFinishedListener() }
- }
+ }.start()
else
onFinishedListener()
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
index d005e58a..8092133b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
@@ -10,7 +10,6 @@ import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
-import ca.allanwang.kau.kpref.StringSet
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.string
import ca.allanwang.kau.utils.tint
@@ -42,10 +41,10 @@ class Keywords @JvmOverloads constructor(
editText.tint(Prefs.textColor)
addIcon.setImageDrawable(GoogleMaterial.Icon.gmd_add.keywordDrawable(context))
addIcon.setOnClickListener {
- if (editText.text.isEmpty()) editText.error = context.string(R.string.empty_keyword)
+ if (editText.text.isNullOrEmpty()) editText.error = context.string(R.string.empty_keyword)
else {
adapter.add(0, KeywordItem(editText.text.toString()))
- editText.text.clear()
+ editText.text?.clear()
}
}
adapter.add(Prefs.notificationKeywords.map { KeywordItem(it) })
@@ -61,11 +60,9 @@ class Keywords @JvmOverloads constructor(
}
fun save() {
- val keywords = adapter.adapterItems.map { it.keyword }
- Prefs.notificationKeywords = StringSet(keywords)
+ Prefs.notificationKeywords = adapter.adapterItems.mapTo(mutableSetOf()) { it.keyword }
}
-
}
private fun IIcon.keywordDrawable(context: Context): Drawable = toDrawable(context, 20, Prefs.textColor)