mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-20 03:25:14 +00:00
Move majority of add site logic to a presenter, add unit tests
This commit is contained in:
parent
f87e1438d2
commit
225434b41a
21 changed files with 622 additions and 155 deletions
|
@ -18,7 +18,7 @@
|
|||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.MainActivity"
|
||||
android:name="com.afollestad.nocknock.ui.main.MainActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
@ -28,14 +28,14 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.AddSiteActivity"
|
||||
android:name="com.afollestad.nocknock.ui.addsite.AddSiteActivity"
|
||||
android:label="@string/add_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Transparent"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.ViewSiteActivity"
|
||||
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
||||
android:label="@string/view_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Ink"
|
||||
|
|
|
@ -12,9 +12,9 @@ import com.afollestad.nocknock.di.DaggerAppComponent
|
|||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.MainActivity
|
||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.Injector
|
||||
import com.afollestad.nocknock.utilities.ext.systemService
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
|
@ -110,10 +110,9 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
|||
|
||||
fun set(newModels: List<ServerModel>) {
|
||||
this.models.clear()
|
||||
if (newModels.isEmpty()) {
|
||||
return
|
||||
if (!newModels.isEmpty()) {
|
||||
this.models.addAll(newModels)
|
||||
}
|
||||
this.models.addAll(newModels)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ import com.afollestad.nocknock.engine.EngineModule
|
|||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.notifications.NotificationsModule
|
||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.MainActivity
|
||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.UtilitiesModule
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.presenters.MainPresenter
|
||||
import com.afollestad.nocknock.presenters.RealMainPresenter
|
||||
import com.afollestad.nocknock.ui.addsite.AddSitePresenter
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
import com.afollestad.nocknock.ui.main.MainPresenter
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
|
@ -20,4 +22,10 @@ abstract class MainBindModule {
|
|||
abstract fun provideMainPresenter(
|
||||
presenter: RealMainPresenter
|
||||
): MainPresenter
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideAddSitePresenter(
|
||||
presenter: RealAddSitePresenter
|
||||
): AddSitePresenter
|
||||
}
|
||||
|
|
|
@ -3,35 +3,29 @@
|
|||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns.WEB_URL
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils.createCircularReveal
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||
import com.afollestad.nocknock.viewcomponents.ext.hide
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
||||
import com.afollestad.nocknock.viewcomponents.ext.show
|
||||
|
@ -42,20 +36,15 @@ import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
|||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_addsite.nameTiLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_addsite.urlTiLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.max
|
||||
import kotlin.properties.Delegates.notNull
|
||||
|
||||
|
@ -76,7 +65,7 @@ fun MainActivity.intentToAdd(
|
|||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||
class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||
|
||||
companion object {
|
||||
private const val REVEAL_DURATION = 300L
|
||||
|
@ -84,8 +73,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
|||
|
||||
private var isClosing: Boolean = false
|
||||
|
||||
@Inject lateinit var serverModelStore: ServerModelStore
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
@Inject lateinit var presenter: AddSitePresenter
|
||||
|
||||
private var revealCx by notNull<Int>()
|
||||
private var revealCy by notNull<Int>()
|
||||
|
@ -94,9 +82,11 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
|||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
injector().injectInto(this)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_addsite)
|
||||
presenter.takeView(this)
|
||||
|
||||
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
|
@ -117,25 +107,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
|||
}
|
||||
|
||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val inputStr = inputUrl.text
|
||||
.toString()
|
||||
.trim()
|
||||
if (inputStr.isEmpty()) {
|
||||
return@setOnFocusChangeListener
|
||||
}
|
||||
|
||||
val uri = Uri.parse(inputStr)
|
||||
if (uri.scheme == null) {
|
||||
inputUrl.setText("http://$inputStr")
|
||||
textUrlWarning.hide()
|
||||
} else if ("http" != uri.scheme && "https" != uri.scheme) {
|
||||
textUrlWarning.show()
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
} else {
|
||||
textUrlWarning.hide()
|
||||
}
|
||||
}
|
||||
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
||||
}
|
||||
|
||||
val validationOptionsAdapter = ArrayAdapter(
|
||||
|
@ -146,23 +118,91 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
|||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
|
||||
responseValidationMode.adapter = validationOptionsAdapter
|
||||
responseValidationMode.onItemSelected { pos ->
|
||||
responseValidationSearchTerm.showOrHide(pos == 1)
|
||||
scriptInputLayout.showOrHide(pos == 2)
|
||||
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
||||
|
||||
validationModeDescription.setText(
|
||||
when (pos) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unknown validation mode position: $pos")
|
||||
}
|
||||
doneBtn.setOnClickListener {
|
||||
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
||||
val validationMode =
|
||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
||||
|
||||
isClosing = true
|
||||
presenter.commit(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationMode.validationContent()
|
||||
)
|
||||
}
|
||||
|
||||
doneBtn.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.dropView()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setLoading() = loadingProgress.setLoading()
|
||||
|
||||
override fun setDoneLoading() = loadingProgress.setDone()
|
||||
|
||||
override fun showOrHideUrlSchemeWarning(show: Boolean) {
|
||||
textUrlWarning.showOrHide(show)
|
||||
if (show) {
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showOrHideValidationSearchTerm(show: Boolean) =
|
||||
responseValidationSearchTerm.showOrHide(show)
|
||||
|
||||
override fun showOrHideScriptInput(show: Boolean) = scriptInputLayout.showOrHide(show)
|
||||
|
||||
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||
|
||||
override fun setInputErrors(errors: InputErrors) {
|
||||
isClosing = false
|
||||
inputName.error = if (errors.name != null) {
|
||||
getString(errors.name!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
inputUrl.error = if (errors.url != null) {
|
||||
getString(errors.url!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
checkIntervalLayout.setError(
|
||||
if (errors.checkInterval != null) {
|
||||
getString(errors.checkInterval!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
responseValidationSearchTerm.error = if (errors.termSearch != null) {
|
||||
getString(errors.termSearch!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
scriptInputLayout.setError(
|
||||
if (errors.javaScript != null) {
|
||||
getString(errors.javaScript!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSiteAdded() {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
||||
}
|
||||
|
||||
override fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
private fun circularRevealActivity() {
|
||||
val circularReveal =
|
||||
createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
||||
|
@ -190,71 +230,6 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
// Done button
|
||||
override fun onClick(view: View) {
|
||||
isClosing = true
|
||||
var newModel = ServerModel(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
status = WAITING,
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
if (newModel.name.isEmpty()) {
|
||||
nameTiLayout.error = getString(R.string.please_enter_name)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
nameTiLayout.error = null
|
||||
}
|
||||
|
||||
if (newModel.url.isEmpty()) {
|
||||
urlTiLayout.error = getString(R.string.please_enter_url)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
urlTiLayout.error = null
|
||||
if (!WEB_URL.matcher(newModel.url).find()) {
|
||||
urlTiLayout.error = getString(R.string.please_enter_valid_url)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
val uri = Uri.parse(newModel.url)
|
||||
if (uri.scheme == null) {
|
||||
newModel = newModel.copy(url = "http://${newModel.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectedCheckInterval = checkIntervalLayout.getSelectedCheckInterval()
|
||||
val selectedValidationMode =
|
||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
||||
|
||||
newModel = newModel.copy(
|
||||
checkInterval = selectedCheckInterval,
|
||||
validationMode = selectedValidationMode,
|
||||
validationContent = selectedValidationMode.validationContent()
|
||||
)
|
||||
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
loadingProgress.setLoading()
|
||||
val storedModel = async(IO) { serverModelStore.put(newModel) }.await()
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = storedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
loadingProgress.setDone()
|
||||
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() = closeActivityWithReveal()
|
||||
|
||||
private fun ValidationMode.validationContent() = when (this) {
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
data class InputErrors(
|
||||
var name: Int? = null,
|
||||
var url: Int? = null,
|
||||
var checkInterval: Int? = null,
|
||||
var termSearch: Int? = null,
|
||||
var javaScript: Int? = null
|
||||
) {
|
||||
@CheckResult fun any(): Boolean {
|
||||
return name != null || url != null || checkInterval != null ||
|
||||
termSearch != null || javaScript != null
|
||||
}
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
interface AddSitePresenter {
|
||||
|
||||
fun takeView(view: AddSiteView)
|
||||
|
||||
fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
)
|
||||
|
||||
fun onValidationModeSelected(index: Int)
|
||||
|
||||
fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
)
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class RealAddSitePresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val checkStatusManager: CheckStatusManager
|
||||
) : AddSitePresenter {
|
||||
|
||||
private var view: AddSiteView? = null
|
||||
|
||||
override fun takeView(view: AddSiteView) {
|
||||
this.view = view
|
||||
}
|
||||
|
||||
override fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
) {
|
||||
if (content.isEmpty() || focused) {
|
||||
return
|
||||
}
|
||||
val url = HttpUrl.parse(content)
|
||||
if (url == null ||
|
||||
(url.scheme() != "http" &&
|
||||
url.scheme() != "https")
|
||||
) {
|
||||
view?.showOrHideUrlSchemeWarning(true)
|
||||
} else {
|
||||
view?.showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidationModeSelected(index: Int) = with(view!!) {
|
||||
showOrHideValidationSearchTerm(index == 1)
|
||||
showOrHideScriptInput(index == 2)
|
||||
setValidationModeDescription(
|
||||
when (index) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unknown validation mode position: $index")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
) {
|
||||
val inputErrors = InputErrors()
|
||||
|
||||
if (name.isEmpty()) {
|
||||
inputErrors.name = R.string.please_enter_name
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
inputErrors.url = R.string.please_enter_url
|
||||
} else if (HttpUrl.parse(url) == null) {
|
||||
inputErrors.url = R.string.please_enter_valid_url
|
||||
}
|
||||
if (checkInterval <= 0) {
|
||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||
}
|
||||
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.termSearch = R.string.please_enter_search_term
|
||||
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||
}
|
||||
|
||||
if (inputErrors.any()) {
|
||||
view?.setInputErrors(inputErrors)
|
||||
return
|
||||
}
|
||||
|
||||
val newModel = ServerModel(
|
||||
name = name,
|
||||
url = url,
|
||||
status = WAITING,
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationContent
|
||||
)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
val storedModel = async(IO) {
|
||||
serverModelStore.put(newModel)
|
||||
}.await()
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = storedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
setDoneLoading()
|
||||
onSiteAdded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dropView() {
|
||||
view = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
interface AddSiteView {
|
||||
|
||||
fun setLoading()
|
||||
|
||||
fun setDoneLoading()
|
||||
|
||||
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||
|
||||
fun showOrHideValidationSearchTerm(show: Boolean)
|
||||
|
||||
fun showOrHideScriptInput(show: Boolean)
|
||||
|
||||
fun setValidationModeDescription(@StringRes res: Int)
|
||||
|
||||
fun setInputErrors(errors: InputErrors)
|
||||
|
||||
fun onSiteAdded()
|
||||
|
||||
fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
)
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityOptions.makeSceneTransitionAnimation
|
||||
|
@ -25,8 +25,8 @@ import com.afollestad.nocknock.adapter.ServerAdapter
|
|||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.presenters.MainPresenter
|
||||
import com.afollestad.nocknock.presenters.MainView
|
||||
import com.afollestad.nocknock.ui.addsite.intentToAdd
|
||||
import com.afollestad.nocknock.ui.viewsite.intentToView
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
|
@ -63,8 +63,10 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
@SuppressLint("CommitPrefEdits")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_main)
|
||||
presenter.takeView(this)
|
||||
|
||||
toolbar.inflateMenu(R.menu.menu_main)
|
||||
toolbar.setOnMenuItemClickListener { item ->
|
||||
|
@ -86,8 +88,6 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
ADD_SITE_RQ
|
||||
)
|
||||
}
|
||||
|
||||
presenter.takeView(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -110,15 +110,21 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
}
|
||||
|
||||
override fun setModels(models: List<ServerModel>) {
|
||||
adapter.set(models)
|
||||
emptyText.showOrHide(models.isEmpty())
|
||||
list.post {
|
||||
adapter.set(models)
|
||||
emptyText.showOrHide(models.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateModel(model: ServerModel) = adapter.update(model)
|
||||
override fun updateModel(model: ServerModel) {
|
||||
list.post { adapter.update(model) }
|
||||
}
|
||||
|
||||
override fun onSiteDeleted(model: ServerModel) {
|
||||
adapter.remove(model)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
list.post {
|
||||
adapter.remove(model)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun scopeWhileAttached(
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.presenters
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
|
@ -58,14 +58,14 @@ class RealMainPresenter @Inject constructor(
|
|||
|
||||
override fun resume() {
|
||||
notificationManager.cancelStatusNotifications()
|
||||
view?.run {
|
||||
view!!.run {
|
||||
setModels(listOf())
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
val models = async(IO) {
|
||||
serverModelStore.get()
|
||||
}
|
||||
setModels(models.await())
|
||||
}.await()
|
||||
setModels(models)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ class RealMainPresenter @Inject constructor(
|
|||
override fun removeSite(site: ServerModel) {
|
||||
checkStatusManager.cancelCheck(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
view?.scopeWhileAttached(Main) {
|
||||
view!!.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
async(IO) { serverModelStore.delete(site) }.await()
|
||||
view?.onSiteDeleted(site)
|
||||
|
@ -95,7 +95,7 @@ class RealMainPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
private fun ensureCheckJobs() {
|
||||
view?.scopeWhileAttached(IO) {
|
||||
view!!.scopeWhileAttached(IO) {
|
||||
launch(coroutineContext) {
|
||||
checkStatusManager.ensureScheduledChecks()
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.presenters
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -39,6 +39,7 @@ import com.afollestad.nocknock.engine.db.ServerModelStore
|
|||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.isHttpOrHttps
|
||||
|
@ -108,11 +109,15 @@ class ViewSiteActivity : AppCompatActivity(),
|
|||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
log("Received broadcast ${intent.action}")
|
||||
log(
|
||||
"Received broadcast ${intent.action}"
|
||||
)
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_MODEL) as? ServerModel
|
||||
if (model != null) {
|
||||
this@ViewSiteActivity.currentModel = model
|
||||
log("Received model update: $currentModel")
|
||||
log(
|
||||
"Received model update: $currentModel"
|
||||
)
|
||||
displayCurrentModel()
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity"
|
||||
tools:context=".ui.main.MainActivity"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
<string name="please_enter_check_interval">Please input a check interval.</string>
|
||||
<string name="please_enter_search_term">Please input a search term.</string>
|
||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||
|
||||
<string name="options">Options</string>
|
||||
<string name="already_checking_sites">Already checking sites!</string>
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
||||
import com.afollestad.nocknock.ui.addsite.InputErrors
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.times
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AddSitePresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore> {
|
||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||
inv.getArgument<ServerModel>(0)
|
||||
}
|
||||
}
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val view = mock<AddSiteView>()
|
||||
|
||||
private val presenter = RealAddSitePresenter(
|
||||
serverModelStore,
|
||||
checkStatusManager
|
||||
)
|
||||
|
||||
@Before fun setup() {
|
||||
doAnswer {
|
||||
val exec = it.getArgument<ScopeReceiver>(1)
|
||||
runBlocking { exec() }
|
||||
Unit
|
||||
}.whenever(view)
|
||||
.scopeWhileAttached(any(), any())
|
||||
|
||||
presenter.takeView(view)
|
||||
}
|
||||
|
||||
@After fun destroy() {
|
||||
presenter.dropView()
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_focused() {
|
||||
presenter.onUrlInputFocusChange(true, "hello")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_empty() {
|
||||
presenter.onUrlInputFocusChange(false, "")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_notHttpHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "ftp://hello.com")
|
||||
verify(view).showOrHideUrlSchemeWarning(true)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_isHttpOrHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "http://hello.com")
|
||||
presenter.onUrlInputFocusChange(false, "https://hello.com")
|
||||
verify(view, times(2)).showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_statusCode() {
|
||||
presenter.onValidationModeSelected(0)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_status_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_termSearch() {
|
||||
presenter.onValidationModeSelected(1)
|
||||
verify(view).showOrHideValidationSearchTerm(true)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_term_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_javaScript() {
|
||||
presenter.onValidationModeSelected(2)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(true)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_javascript_desc)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun onValidationModeSelected_other() {
|
||||
presenter.onValidationModeSelected(3)
|
||||
}
|
||||
|
||||
@Test fun commit_nameError() {
|
||||
presenter.commit(
|
||||
"",
|
||||
"https://test.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.name).isEqualTo(R.string.please_enter_name)
|
||||
}
|
||||
|
||||
@Test fun commit_urlEmptyError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_url)
|
||||
}
|
||||
|
||||
@Test fun commit_urlFormatError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"ftp://hello.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_valid_url)
|
||||
}
|
||||
|
||||
@Test fun commit_checkIntervalError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
-1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.checkInterval).isEqualTo(R.string.please_enter_check_interval)
|
||||
}
|
||||
|
||||
@Test fun commit_termSearchError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
TERM_SEARCH,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.termSearch).isEqualTo(R.string.please_enter_search_term)
|
||||
}
|
||||
|
||||
@Test fun commit_javaScript_error() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
JAVASCRIPT,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.javaScript).isEqualTo(R.string.please_enter_javaScript)
|
||||
}
|
||||
|
||||
@Test fun commit_success() = runBlocking {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).put(modelCaptor.capture())
|
||||
val model = modelCaptor.firstValue
|
||||
verify(view, never()).setInputErrors(any())
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true,
|
||||
fromFinishingJob = false
|
||||
)
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).onSiteAdded()
|
||||
}
|
||||
}
|
|
@ -13,8 +13,8 @@ import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTIO
|
|||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.presenters.MainView
|
||||
import com.afollestad.nocknock.presenters.RealMainPresenter
|
||||
import com.afollestad.nocknock.ui.main.MainView
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
|
|
|
@ -78,7 +78,7 @@ class RealNockNotificationManager @Inject constructor(
|
|||
|
||||
log("Posting status notification for site ${model.id}...")
|
||||
val viewSiteActivityCls =
|
||||
Class.forName("com.afollestad.nocknock.ui.ViewSiteActivity")
|
||||
Class.forName("com.afollestad.nocknock.ui.viewsite.ViewSiteActivity")
|
||||
val openIntent = Intent(app, viewSiteActivityCls).apply {
|
||||
putExtra(KEY_MODEL, model)
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
|
|
|
@ -49,6 +49,10 @@ class CheckIntervalLayout(
|
|||
spinner.adapter = spinnerAdapter
|
||||
}
|
||||
|
||||
fun setError(error: String?) {
|
||||
input.error = error
|
||||
}
|
||||
|
||||
fun set(interval: Long) {
|
||||
when {
|
||||
interval >= WEEK -> {
|
||||
|
|
|
@ -11,7 +11,9 @@ import android.widget.HorizontalScrollView
|
|||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenInt
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
||||
import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text
|
||||
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
|
@ -33,6 +35,11 @@ class JavaScriptInputLayout(
|
|||
inflate(context, R.layout.javascript_input_layout, this)
|
||||
}
|
||||
|
||||
fun setError(error: String?) {
|
||||
error_text.showOrHide(error != null)
|
||||
error_text.text = error
|
||||
}
|
||||
|
||||
fun setCode(code: String?) {
|
||||
if (code.isNullOrEmpty()) {
|
||||
setDefaultCode()
|
||||
|
@ -41,9 +48,9 @@ class JavaScriptInputLayout(
|
|||
userInput.setText(code.trim())
|
||||
}
|
||||
|
||||
fun setDefaultCode() = userInput.setText(R.string.default_js)
|
||||
|
||||
@CheckResult fun getCode() = userInput.trimmedText()
|
||||
|
||||
fun clear() = userInput.setText("")
|
||||
|
||||
private fun setDefaultCode() = userInput.setText(R.string.default_js)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,17 @@
|
|||
android:textSize="@dimen/code_font_size"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:visibility="gone"
|
||||
tools:text="Please enter some JavaScript!"
|
||||
tools:visibility="visible"
|
||||
style="@style/NockText.Error"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
||||
|
|
|
@ -39,4 +39,10 @@
|
|||
<item name="android:textColorHint">?android:textColorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="NockText.Error">
|
||||
<item name="android:textSize">@dimen/caption_font_size</item>
|
||||
<item name="android:fontFamily">@font/lato_light</item>
|
||||
<item name="android:textColor">@color/md_red</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue