Begin to modularize reused view layouts, switch to Material Components theme

This commit is contained in:
Aidan Follestad 2018-11-30 15:34:40 -08:00
parent c7096e8746
commit 6dfff5bb12
41 changed files with 502 additions and 365 deletions

1
.idea/modules.xml generated
View file

@ -8,6 +8,7 @@
<module fileurl="file://$PROJECT_DIR$/nock-nock.iml" filepath="$PROJECT_DIR$/nock-nock.iml" />
<module fileurl="file://$PROJECT_DIR$/notifications/notifications.iml" filepath="$PROJECT_DIR$/notifications/notifications.iml" />
<module fileurl="file://$PROJECT_DIR$/utilities/utilities.iml" filepath="$PROJECT_DIR$/utilities/utilities.iml" />
<module fileurl="file://$PROJECT_DIR$/viewcomponents/viewcomponents.iml" filepath="$PROJECT_DIR$/viewcomponents/viewcomponents.iml" />
</modules>
</component>
</project>

View file

@ -22,6 +22,7 @@ dependencies {
implementation project(':utilities')
implementation project(':engine')
implementation project(':notifications')
implementation project(':viewcomponents')
implementation 'androidx.appcompat:appcompat:' + versions.androidx
implementation 'androidx.recyclerview:recyclerview:' + versions.androidx

View file

@ -25,33 +25,30 @@ 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.utilities.ext.conceal
import com.afollestad.nocknock.utilities.ext.hide
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.ext.onEnd
import com.afollestad.nocknock.utilities.ext.onItemSelected
import com.afollestad.nocknock.utilities.ext.onLayout
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
import com.afollestad.nocknock.utilities.ext.show
import com.afollestad.nocknock.utilities.ext.showOrHide
import com.afollestad.nocknock.utilities.ext.textAsLong
import com.afollestad.nocknock.utilities.ext.trimmedText
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalInput
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalSpinner
import kotlinx.android.synthetic.main.activity_addsite.content_loading_progress
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
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
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.android.synthetic.main.include_script_input.responseValidationScript
import kotlinx.android.synthetic.main.include_script_input.responseValidationScriptInput
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
@ -97,14 +94,6 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
rootView.onLayout { circularRevealActivity() }
}
val intervalOptionsAdapter = ArrayAdapter(
this,
R.layout.list_item_spinner,
resources.getStringArray(R.array.interval_options)
)
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
checkIntervalSpinner.adapter = intervalOptionsAdapter
inputUrl.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val inputStr = inputUrl.text
@ -137,7 +126,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
responseValidationMode.adapter = validationOptionsAdapter
responseValidationMode.onItemSelected { pos ->
responseValidationSearchTerm.showOrHide(pos == 1)
responseValidationScript.showOrHide(pos == 2)
scriptInputLayout.showOrHide(pos == 2)
validationModeDescription.setText(
when (pos) {
@ -200,14 +189,14 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
// Done button
override fun onClick(view: View) {
isClosing = true
var model = ServerModel(
var newModel = ServerModel(
name = inputName.trimmedText(),
url = inputUrl.trimmedText(),
status = WAITING,
validationMode = STATUS_CODE
)
if (model.name.isEmpty()) {
if (newModel.name.isEmpty()) {
nameTiLayout.error = getString(R.string.please_enter_name)
isClosing = false
return
@ -215,42 +204,42 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
nameTiLayout.error = null
}
if (model.url.isEmpty()) {
if (newModel.url.isEmpty()) {
urlTiLayout.error = getString(R.string.please_enter_url)
isClosing = false
return
} else {
urlTiLayout.error = null
if (!WEB_URL.matcher(model.url).find()) {
if (!WEB_URL.matcher(newModel.url).find()) {
urlTiLayout.error = getString(R.string.please_enter_valid_url)
isClosing = false
return
} else {
val uri = Uri.parse(model.url)
val uri = Uri.parse(newModel.url)
if (uri.scheme == null) {
model = model.copy(url = "http://${model.url}")
newModel = newModel.copy(url = "http://${newModel.url}")
}
}
}
val parsedCheckInterval = getParsedCheckInterval()
val selectedCheckInterval = checkIntervalLayout.getSelectedCheckInterval()
val selectedValidationMode = getSelectedValidationMode()
val selectedValidationContent = getSelectedValidationContent()
model = model.copy(
checkInterval = parsedCheckInterval,
lastCheck = currentTimeMillis() - parsedCheckInterval,
newModel = newModel.copy(
checkInterval = selectedCheckInterval,
lastCheck = currentTimeMillis() - selectedCheckInterval,
validationMode = selectedValidationMode,
validationContent = selectedValidationContent
)
rootView.scopeWhileAttached(Main) {
launch(coroutineContext) {
content_loading_progress.show()
val storedModel = async(IO) { serverModelStore.put(model) }.await()
loadingProgress.setLoading()
val storedModel = async(IO) { serverModelStore.put(newModel) }.await()
checkStatusManager.cancelCheck(storedModel)
checkStatusManager.scheduleCheck(storedModel, rightNow = true)
content_loading_progress.hide()
loadingProgress.setDone()
setResult(RESULT_OK)
finish()
@ -261,16 +250,6 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
override fun onBackPressed() = closeActivityWithReveal()
private fun getParsedCheckInterval(): Long {
val intervalInput = checkIntervalInput.textAsLong()
return when (checkIntervalSpinner.selectedItemPosition) {
0 -> intervalInput * (60 * 1000)
1 -> intervalInput * (60 * 60 * 1000)
2 -> intervalInput * (60 * 60 * 24 * 1000)
else -> intervalInput * (60 * 60 * 24 * 7 * 1000)
}
}
private fun getSelectedValidationMode() = when (responseValidationMode.selectedItemPosition) {
0 -> STATUS_CODE
1 -> TERM_SEARCH
@ -285,7 +264,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
private fun getSelectedValidationContent() = when (responseValidationMode.selectedItemPosition) {
0 -> null
1 -> responseValidationSearchTerm.trimmedText()
2 -> responseValidationScriptInput.trimmedText()
2 -> scriptInputLayout.getCode()
else -> {
throw IllegalStateException(
"Unexpected validation mode index: ${responseValidationMode.selectedItemPosition}"

View file

@ -44,8 +44,8 @@ import com.afollestad.nocknock.utilities.ext.onEnd
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
import com.afollestad.nocknock.utilities.ext.show
import com.afollestad.nocknock.utilities.ext.showOrHide
import com.afollestad.nocknock.viewcomponents.ext.show
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import com.afollestad.nocknock.utilities.util.MathUtil.bezierCurve
import kotlinx.android.synthetic.main.activity_main.emptyText
import kotlinx.android.synthetic.main.activity_main.fab

View file

@ -35,46 +35,38 @@ 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.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK
import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.ext.hide
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.ext.isHttpOrHttps
import com.afollestad.nocknock.utilities.ext.onItemSelected
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
import com.afollestad.nocknock.utilities.ext.show
import com.afollestad.nocknock.utilities.ext.showOrHide
import com.afollestad.nocknock.utilities.ext.textAsLong
import com.afollestad.nocknock.utilities.ext.trimmedText
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalInput
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalSpinner
import kotlinx.android.synthetic.main.activity_viewsite.content_loading_progress
import com.afollestad.nocknock.viewcomponents.ext.hide
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
import com.afollestad.nocknock.viewcomponents.ext.show
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
import kotlinx.android.synthetic.main.activity_viewsite.inputName
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
import kotlinx.android.synthetic.main.activity_viewsite.rootView
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
import kotlinx.android.synthetic.main.include_script_input.responseValidationScript
import kotlinx.android.synthetic.main.include_script_input.responseValidationScriptInput
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.lang.System.currentTimeMillis
import javax.inject.Inject
import kotlin.math.ceil
private const val KEY_VIEW_MODEL = "site_model"
@ -130,14 +122,6 @@ class ViewSiteActivity : AppCompatActivity(),
setOnMenuItemClickListener(this@ViewSiteActivity)
}
val intervalOptionsAdapter = ArrayAdapter(
this,
R.layout.list_item_spinner,
resources.getStringArray(R.array.interval_options)
)
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
checkIntervalSpinner.adapter = intervalOptionsAdapter
inputUrl.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val inputStr = inputUrl.text
@ -170,7 +154,7 @@ class ViewSiteActivity : AppCompatActivity(),
responseValidationMode.onItemSelected { pos ->
responseValidationSearchTerm.showOrHide(pos == 1)
responseValidationScript.showOrHide(pos == 2)
scriptInputLayout.showOrHide(pos == 2)
validationModeDescription.setText(
when (pos) {
@ -213,45 +197,13 @@ class ViewSiteActivity : AppCompatActivity(),
if (this.checkInterval == 0L) {
textNextCheck.setText(R.string.none_turned_off)
checkIntervalInput.setText("")
checkIntervalSpinner.setSelection(0)
checkIntervalLayout.clear()
} else {
var lastCheck = this.lastCheck
if (lastCheck == 0L) {
lastCheck = currentTimeMillis()
}
textNextCheck.text = (lastCheck + this.checkInterval).formatDate()
when {
this.checkInterval >= WEEK -> {
checkIntervalInput.setText(
ceil((this.checkInterval.toFloat() / WEEK).toDouble()).toInt().toString()
)
checkIntervalSpinner.setSelection(3)
}
this.checkInterval >= DAY -> {
checkIntervalInput.setText(
ceil((this.checkInterval.toFloat() / DAY.toFloat()).toDouble()).toInt().toString()
)
checkIntervalSpinner.setSelection(2)
}
this.checkInterval >= HOUR -> {
checkIntervalInput.setText(
ceil((this.checkInterval.toFloat() / HOUR.toFloat()).toDouble()).toInt().toString()
)
checkIntervalSpinner.setSelection(1)
}
this.checkInterval >= MINUTE -> {
checkIntervalInput.setText(
ceil((this.checkInterval.toFloat() / MINUTE.toFloat()).toDouble()).toInt().toString()
)
checkIntervalSpinner.setSelection(0)
}
else -> {
checkIntervalInput.setText("0")
checkIntervalSpinner.setSelection(0)
}
}
}
responseValidationMode.setSelection(validationMode.value - 1)
@ -259,13 +211,11 @@ class ViewSiteActivity : AppCompatActivity(),
when (this.validationMode) {
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
JAVASCRIPT -> {
responseValidationScriptInput.setText(
this.validationContent ?: getString(R.string.default_js)
)
scriptInputLayout.setCode(this.validationContent)
}
else -> {
responseValidationSearchTerm.setText("")
responseValidationScriptInput.setText("")
scriptInputLayout.clear()
}
}
@ -316,13 +266,13 @@ class ViewSiteActivity : AppCompatActivity(),
}
}
val parsedCheckInterval = getParsedCheckInterval()
val selectedCheckInterval = checkIntervalLayout.getSelectedCheckInterval()
val selectedValidationMode = getSelectedValidationMode()
val selectedValidationContent = getSelectedValidationContent()
currentModel = currentModel.copy(
checkInterval = parsedCheckInterval,
lastCheck = currentTimeMillis() - parsedCheckInterval,
checkInterval = selectedCheckInterval,
lastCheck = currentTimeMillis() - selectedCheckInterval,
validationMode = selectedValidationMode,
validationContent = selectedValidationContent
)
@ -334,10 +284,10 @@ class ViewSiteActivity : AppCompatActivity(),
override fun onClick(view: View) {
rootView.scopeWhileAttached(Main) {
launch(coroutineContext) {
content_loading_progress.show()
loadingProgress.setLoading()
if (!updateModelFromInput(true)) {
// Validation didn't pass
content_loading_progress.hide()
loadingProgress.setDone()
return@launch
}
@ -345,7 +295,7 @@ class ViewSiteActivity : AppCompatActivity(),
checkStatusManager.cancelCheck(currentModel)
checkStatusManager.scheduleCheck(currentModel, rightNow = true)
content_loading_progress.hide()
loadingProgress.setDone()
setResult(RESULT_OK)
finish()
}
@ -357,7 +307,7 @@ class ViewSiteActivity : AppCompatActivity(),
R.id.refresh -> {
rootView.scopeWhileAttached(Main) {
launch(coroutineContext) {
content_loading_progress.show()
loadingProgress.setLoading()
updateModelFromInput(false)
currentModel = currentModel.copy(status = WAITING)
displayCurrentModel()
@ -366,7 +316,7 @@ class ViewSiteActivity : AppCompatActivity(),
checkStatusManager.cancelCheck(currentModel)
checkStatusManager.scheduleCheck(currentModel, rightNow = true)
content_loading_progress.hide()
loadingProgress.setDone()
}
}
return true
@ -400,9 +350,9 @@ class ViewSiteActivity : AppCompatActivity(),
private fun performRemoveSite(model: ServerModel) {
rootView.scopeWhileAttached(Main) {
launch(coroutineContext) {
content_loading_progress.show()
loadingProgress.setLoading()
async(IO) { serverModelStore.delete(model) }.await()
content_loading_progress.hide()
loadingProgress.setDone()
finish()
}
}
@ -413,16 +363,6 @@ class ViewSiteActivity : AppCompatActivity(),
item.isEnabled = currentModel.status != CHECKING && currentModel.status != WAITING
}
private fun getParsedCheckInterval(): Long {
val intervalInput = checkIntervalInput.textAsLong()
return when (checkIntervalSpinner.selectedItemPosition) {
0 -> intervalInput * (60 * 1000)
1 -> intervalInput * (60 * 60 * 1000)
2 -> intervalInput * (60 * 60 * 24 * 1000)
else -> intervalInput * (60 * 60 * 24 * 7 * 1000)
}
}
private fun getSelectedValidationMode() = when (responseValidationMode.selectedItemPosition) {
0 -> STATUS_CODE
1 -> TERM_SEARCH
@ -437,7 +377,7 @@ class ViewSiteActivity : AppCompatActivity(),
private fun getSelectedValidationContent() = when (responseValidationMode.selectedItemPosition) {
0 -> null
1 -> responseValidationSearchTerm.trimmedText()
2 -> responseValidationScriptInput.trimmedText()
2 -> scriptInputLayout.getCode()
else -> {
throw IllegalStateException(
"Unexpected validation mode index: ${responseValidationMode.selectedItemPosition}"

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_green"/>
<stroke android:color="#424242"/>
<size
android:width="@dimen/list_circle_size"
android:height="@dimen/list_circle_size"/>
</shape>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_red"/>
<stroke android:color="#424242"/>
<size
android:width="@dimen/list_circle_size"
android:height="@dimen/list_circle_size"/>
</shape>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_yellow"/>
<stroke android:color="#424242"/>
<size
android:width="@dimen/list_circle_size"
android:height="@dimen/list_circle_size"/>
</shape>

View file

@ -96,62 +96,13 @@
tools:text="Warning: this app checks for server availability with HTTP requests. It's recommended that you use an HTTP URL."
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset"
android:background="@color/dividerColorDark"
/>
<include layout="@layout/include_divider"/>
<TextView
android:id="@+id/checkIntervalLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset"
android:fontFamily="sans-serif"
android:text="@string/check_interval"
android:textColor="?colorAccent"
android:textSize="@dimen/caption_font_size"
/>
<LinearLayout
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
android:id="@+id/checkIntervalLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2"
>
<EditText
android:id="@+id/checkIntervalInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginEnd="@dimen/content_inset_half"
android:layout_marginStart="-4dp"
android:layout_weight="1"
android:fontFamily="sans-serif-light"
android:hint="0"
android:inputType="number"
android:textSize="@dimen/body_font_size"
tools:ignore="HardcodedText,LabelFor"
/>
<Spinner
android:id="@+id/checkIntervalSpinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="-4dp"
android:layout_weight="1"
/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset"
android:background="@color/dividerColorDark"
/>
<TextView
@ -186,7 +137,14 @@
tools:ignore="Autofill"
/>
<include layout="@layout/include_script_input"/>
<com.afollestad.nocknock.viewcomponents.JavaScriptInputLayout
android:id="@+id/scriptInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark"
/>
<TextView
android:id="@+id/validationModeDescription"
@ -199,15 +157,13 @@
android:textSize="@dimen/body_font_size"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/doneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:layout_marginTop="@dimen/content_inset"
android:text="@string/done"
android:textColor="#fff"
style="@style/AccentButton"
/>
</LinearLayout>
@ -216,21 +172,10 @@
</LinearLayout>
<FrameLayout
android:id="@+id/content_loading_progress"
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
android:id="@+id/loadingProgress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#40000000"
android:visibility="gone"
>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="?android:progressBarStyleLarge"
/>
</FrameLayout>
/>
</FrameLayout>

View file

@ -45,7 +45,7 @@
android:orientation="horizontal"
>
<com.afollestad.nocknock.views.StatusImageView
<com.afollestad.nocknock.viewcomponents.StatusImageView
android:id="@+id/iconStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -110,56 +110,19 @@
</LinearLayout>
<View
<include
layout="@layout/include_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset_less"
android:background="@color/dividerColorDark"
/>
<TextView
android:id="@+id/checkIntervalLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset"
android:fontFamily="sans-serif"
android:text="@string/check_interval"
android:textColor="?colorAccent"
android:textSize="@dimen/caption_font_size"
/>
<LinearLayout
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
android:id="@+id/checkIntervalLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2"
>
<EditText
android:id="@+id/checkIntervalInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginEnd="@dimen/content_inset_half"
android:layout_marginStart="-4dp"
android:layout_weight="1"
android:fontFamily="sans-serif-light"
android:hint="0"
android:inputType="number"
android:textSize="@dimen/body_font_size"
tools:ignore="HardcodedText,LabelFor"
/>
<Spinner
android:id="@+id/checkIntervalSpinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="-4dp"
android:layout_weight="1"
/>
</LinearLayout>
android:layout_marginTop="@dimen/content_inset"
/>
<View
android:layout_width="match_parent"
@ -197,9 +160,17 @@
android:hint="@string/search_term"
android:textSize="@dimen/body_font_size"
android:visibility="gone"
tools:ignore="Autofill,TextFields"
/>
<include layout="@layout/include_script_input"/>
<com.afollestad.nocknock.viewcomponents.JavaScriptInputLayout
android:id="@+id/scriptInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark"
/>
<TextView
android:id="@+id/validationModeDescription"
@ -212,12 +183,7 @@
android:textSize="@dimen/body_font_size"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset"
android:background="@color/dividerColorDark"
/>
<include layout="@layout/include_divider"/>
<TextView
android:layout_width="wrap_content"
@ -259,15 +225,13 @@
tools:text="In 2 hours"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/doneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:layout_marginTop="@dimen/content_inset_more"
android:text="@string/save"
android:textColor="#fff"
style="@style/AccentButton"
/>
</LinearLayout>
@ -276,21 +240,10 @@
</LinearLayout>
<FrameLayout
android:id="@+id/content_loading_progress"
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
android:id="@+id/loadingProgress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#40000000"
android:visibility="gone"
>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="?android:progressBarStyleLarge"
/>
</FrameLayout>
/>
</FrameLayout>

View file

@ -12,7 +12,7 @@
android:paddingTop="@dimen/content_inset_less"
>
<com.afollestad.nocknock.views.StatusImageView
<com.afollestad.nocknock.viewcomponents.StatusImageView
android:id="@+id/iconStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="interval_options">
<item>Minute(s)</item>
<item>Hour(s)</item>
<item>Day(s)</item>
<item>Week(s)</item>
</string-array>
<string-array name="site_long_options" translatable="false">
<item>@string/refresh_status</item>
<item>@string/remove_site</item>

View file

@ -7,10 +7,4 @@
<color name="dividerColor">#EEEEEE</color>
<color name="md_red">#E53935</color>
<color name="md_yellow">#FDD835</color>
<color name="md_green">#43A047</color>
<color name="dividerColorDark">#37474F</color>
</resources>

View file

@ -2,20 +2,11 @@
<dimen name="title_font_size">20sp</dimen>
<dimen name="medium_text_size">16sp</dimen>
<dimen name="body_font_size">14sp</dimen>
<dimen name="caption_font_size">12sp</dimen>
<dimen name="empty_text_size">26sp</dimen>
<dimen name="content_inset_half">8dp</dimen>
<dimen name="content_inset_less">12dp</dimen>
<dimen name="content_inset">16dp</dimen>
<dimen name="content_inset_more">24dp</dimen>
<dimen name="list_circle_size">42dp</dimen>
<dimen name="list_text_spacing">4dp</dimen>
<dimen name="fab_elevation">4dp</dimen>
<dimen name="fab_elevation_pressed">8dp</dimen>
<dimen name="button_height">52dp</dimen>
<dimen name="code_font_size">14sp</dimen>
</resources>

View file

@ -19,7 +19,6 @@
<string name="add_site">Add Site</string>
<string name="site_name">Site Name</string>
<string name="site_url">Site URL</string>
<string name="check_interval">Check Interval</string>
<string name="done">Done</string>
<string name="please_enter_name">Please enter a name!</string>
<string name="please_enter_url">Please enter a URL.</string>
@ -42,9 +41,6 @@
<string name="warning_http_url">
Warning: this app checks for server availability with HTTP requests. It\'s recommended that you use an HTTP URL.
</string>
<string name="default_js">var responseObj = JSON.parse(response);\nreturn responseObj.success === true;</string>
<string name="function_declaration">function validate(response) {</string>
<string name="function_end">}</string>
<string name="response_validation_mode">Response Validation Mode</string>
<string name="search_term">Search term…</string>

View file

@ -1,6 +1,6 @@
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
@ -12,7 +12,7 @@
<item name="android:textColorSecondary">#727272</item>
</style>
<style name="AppTheme.Ink" parent="Theme.AppCompat.NoActionBar">
<style name="AppTheme.Ink" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
@ -27,7 +27,7 @@
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="AccentButton" parent="Widget.AppCompat.Button.Colored">
<style name="AccentButton" parent="Widget.MaterialComponents.Button">
<item name="android:textColor">#fff</item>
<item name="android:colorButtonNormal">@color/colorAccent</item>
</style>

View file

@ -1 +1 @@
include ':app', ':engine', ':notifications', ':data', ':utilities'
include ':app', ':engine', ':notifications', ':data', ':utilities', ':viewcomponents'

1
viewcomponents/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,29 @@
apply from: '../dependencies.gradle'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.compileSdk
versionCode versions.publishVersionCode
versionName versions.publishVersion
}
}
dependencies {
implementation project(':utilities')
implementation project(':data')
implementation 'androidx.appcompat:appcompat:' + versions.androidx
api 'com.squareup.okhttp3:okhttp:' + versions.okHttp
implementation 'com.google.dagger:dagger:' + versions.dagger
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
}
apply from: '../spotless.gradle'

View file

@ -0,0 +1,2 @@
<manifest
package="com.afollestad.nocknock.viewcomponents"/>

View file

@ -0,0 +1,103 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.viewcomponents
import android.content.Context
import android.util.AttributeSet
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import androidx.annotation.CheckResult
import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK
import com.afollestad.nocknock.viewcomponents.R.array
import com.afollestad.nocknock.viewcomponents.ext.textAsLong
import kotlinx.android.synthetic.main.check_interval_layout.view.input
import kotlinx.android.synthetic.main.check_interval_layout.view.spinner
import kotlin.math.ceil
/** @author Aidan Follestad (afollestad) */
class CheckIntervalLayout(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
companion object {
private const val INDEX_MINUTE = 0
private const val INDEX_HOUR = 1
private const val INDEX_DAY = 2
private const val INDEX_WEEK = 3
}
init {
orientation = VERTICAL
inflate(context, R.layout.check_interval_layout, this)
}
override fun onFinishInflate() {
super.onFinishInflate()
val spinnerAdapter = ArrayAdapter(
context,
R.layout.list_item_spinner,
resources.getStringArray(array.interval_options)
)
spinnerAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
spinner.adapter = spinnerAdapter
}
fun set(interval: Long) {
when {
interval >= WEEK -> {
input.setText(calculateDisplayValue(interval, WEEK))
spinner.setSelection(3)
}
interval >= DAY -> {
input.setText(calculateDisplayValue(interval, DAY))
spinner.setSelection(2)
}
interval >= HOUR -> {
input.setText(calculateDisplayValue(interval, HOUR))
spinner.setSelection(1)
}
interval >= MINUTE -> {
input.setText(calculateDisplayValue(interval, MINUTE))
spinner.setSelection(0)
}
else -> {
input.setText("0")
spinner.setSelection(0)
}
}
}
fun clear() {
input.setText("")
spinner.setSelection(0)
}
@CheckResult fun getSelectedCheckInterval(): Long {
val intervalInput = input.textAsLong()
val spinnerPos = spinner.selectedItemPosition
return when (spinnerPos) {
INDEX_MINUTE -> intervalInput * MINUTE
INDEX_HOUR -> intervalInput * HOUR
INDEX_DAY -> intervalInput * DAY
INDEX_WEEK -> intervalInput * WEEK
else -> throw IllegalStateException("Unexpected index: $spinnerPos")
}
}
private fun calculateDisplayValue(
interval: Long,
by: Long
): String {
val intervalFloat = interval.toFloat()
val byFloat = by.toFloat()
return ceil(intervalFloat / byFloat).toInt()
.toString()
}
}

View file

@ -0,0 +1,49 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.viewcomponents
import android.content.Context
import android.util.AttributeSet
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.trimmedText
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
/** @author Aidan Follestad (afollestad) */
class JavaScriptInputLayout(
context: Context,
attrs: AttributeSet? = null
) : HorizontalScrollView(context, attrs) {
init {
val contentInset = dimenInt(R.dimen.content_inset)
val contentInsetHalf = dimenInt(R.dimen.content_inset_half)
setPadding(
contentInsetHalf, // left
contentInset, // top
contentInsetHalf, // right
contentInset // bottom
)
elevation = dimenFloat(R.dimen.default_elevation)
inflate(context, R.layout.javascript_input_layout, this)
}
fun setCode(code: String?) {
if (code.isNullOrEmpty()) {
setDefaultCode()
return
}
userInput.setText(code.trim())
}
fun setDefaultCode() = userInput.setText(R.string.default_js)
@CheckResult fun getCode() = userInput.trimmedText()
fun clear() = userInput.setText("")
}

View file

@ -0,0 +1,40 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.viewcomponents
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import com.afollestad.nocknock.viewcomponents.ext.hide
import com.afollestad.nocknock.viewcomponents.ext.show
/** @author Aidan Follestad (@afollestad) */
class LoadingIndicatorFrame(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
companion object {
private const val SHOW_DELAY_MS = 200L
}
private val showRunnable = Runnable { show() }
init {
setBackgroundColor(ContextCompat.getColor(context, R.color.loading_indicator_frame_background))
hide() // hide self by default
inflate(context, R.layout.loading_indicator_frame, this)
}
fun setLoading() {
handler.postDelayed(showRunnable, SHOW_DELAY_MS)
}
fun setDone() {
handler.removeCallbacks(showRunnable)
hide()
}
}

View file

@ -3,12 +3,11 @@
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.views
package com.afollestad.nocknock.viewcomponents
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.ServerStatus
import com.afollestad.nocknock.data.ServerStatus.CHECKING
import com.afollestad.nocknock.data.ServerStatus.ERROR

View file

@ -3,7 +3,7 @@
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.ext
package com.afollestad.nocknock.viewcomponents.ext
import android.view.View
import android.view.View.GONE
@ -13,6 +13,7 @@ import android.view.ViewTreeObserver
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.DimenRes
fun View.show() {
visibility = VISIBLE
@ -59,3 +60,7 @@ fun View.onLayout(cb: () -> Unit) {
})
}
}
fun View.dimenFloat(@DimenRes res: Int) = resources.getDimension(res)
fun View.dimenInt(@DimenRes res: Int) = resources.getDimensionPixelSize(res)

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_green"/>
<stroke android:color="#424242"/>
<size
android:height="@dimen/list_circle_size"
android:width="@dimen/list_circle_size"/>
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_red"/>
<stroke android:color="#424242"/>
<size
android:height="@dimen/list_circle_size"
android:width="@dimen/list_circle_size"/>
</shape>

View file

@ -1,8 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:viewportWidth="24.0"
android:width="24dp">
<path
android:fillColor="#fff"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>

View file

@ -1,8 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:viewportWidth="24.0"
android:width="24dp">
<path
android:fillColor="#fff"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>

View file

@ -1,8 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:viewportWidth="24.0"
android:width="24dp">
<path
android:fillColor="#fff"
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/md_yellow"/>
<stroke android:color="#424242"/>
<size
android:height="@dimen/list_circle_size"
android:width="@dimen/list_circle_size"/>
</shape>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:text="@string/check_interval"
android:textColor="?colorAccent"
android:textSize="@dimen/caption_font_size"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2"
>
<EditText
android:id="@+id/input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginEnd="@dimen/content_inset_half"
android:layout_marginStart="-4dp"
android:layout_weight="1"
android:fontFamily="sans-serif-light"
android:hint="0"
android:inputType="number"
android:textSize="@dimen/body_font_size"
tools:ignore="Autofill,HardcodedText,LabelFor"
/>
<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="-4dp"
android:layout_weight="1"
/>
</LinearLayout>
</merge>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset"
android:background="@color/dividerColorDark"
/>

View file

@ -1,19 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/responseValidationScript"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark"
android:elevation="@dimen/fab_elevation"
android:paddingBottom="@dimen/content_inset"
android:paddingLeft="@dimen/content_inset_half"
android:paddingRight="@dimen/content_inset_half"
android:paddingTop="@dimen/content_inset"
android:scrollbars="none"
tools:ignore="UnusedAttribute"
>
@ -34,7 +24,7 @@
/>
<EditText
android:id="@+id/responseValidationScriptInput"
android:id="@+id/userInput"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
@ -62,4 +52,4 @@
</LinearLayout>
</HorizontalScrollView>
</merge>

View file

@ -6,4 +6,5 @@
android:fontFamily="sans-serif-light"
android:gravity="center_vertical|start"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/body_font_size" />
android:textSize="@dimen/body_font_size"
/>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="?android:progressBarStyleLarge"
/>
</merge>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="interval_options">
<item>Minute(s)</item>
<item>Hour(s)</item>
<item>Day(s)</item>
<item>Week(s)</item>
</string-array>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="dividerColorDark">#37474F</color>
<color name="loading_indicator_frame_background">#40000000</color>
<color name="md_red">#E53935</color>
<color name="md_yellow">#FDD835</color>
<color name="md_green">#43A047</color>
</resources>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="list_circle_size">42dp</dimen>
<dimen name="default_elevation">4dp</dimen>
<dimen name="content_inset_half">8dp</dimen>
<dimen name="content_inset_less">12dp</dimen>
<dimen name="content_inset">16dp</dimen>
<dimen name="content_inset_more">24dp</dimen>
<dimen name="code_font_size">14sp</dimen>
<dimen name="body_font_size">14sp</dimen>
<dimen name="caption_font_size">12sp</dimen>
<dimen name="button_height">52dp</dimen>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="default_js">var responseObj = JSON.parse(response);\nreturn responseObj.success === true;</string>
<string name="function_declaration">function validate(response) {</string>
<string name="function_end">}</string>
<string name="check_interval">Check Interval</string>
</resources>