Tag system, resolves #13

This commit is contained in:
Aidan Follestad 2019-01-07 22:52:31 -08:00
commit 002149cd3f
26 changed files with 326 additions and 20 deletions

View file

@ -66,9 +66,6 @@ dependencies {
// afollestad // afollestad
implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
// Misc
implementation 'com.github.okdroid:checkablechipview:' + versions.chipView
// Debugging // Debugging
implementation 'com.jakewharton.timber:timber:' + versions.timber implementation 'com.jakewharton.timber:timber:' + versions.timber
implementation("com.crashlytics.sdk.android:crashlytics:${versions.fabric}") { implementation("com.crashlytics.sdk.android:crashlytics:${versions.fabric}") {

View file

@ -0,0 +1,115 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.adapter
import android.graphics.Color.WHITE
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.TagAdapter.TagViewHolder
import kotlinx.android.synthetic.main.list_item_tag.view.chip
typealias TagsListener = (tags: List<String>) -> Unit
/** @author Aidan Follestad (@afollestad) */
class TagAdapter(
private val listener: TagsListener
) : RecyclerView.Adapter<TagViewHolder>() {
private val tags = mutableListOf<String>()
private val checked = mutableListOf<Int>()
fun set(tags: List<String>) {
this.tags.run {
clear()
addAll(tags)
}
notifyDataSetChanged()
}
fun toggleChecked(index: Int) {
if (checked.contains(index)) {
checked.remove(index)
} else {
checked.add(index)
}
notifyItemChanged(index)
listener.invoke(getCheckedTags())
}
private fun getCheckedTags(): List<String> {
return mutableListOf<String>().apply {
checked.forEach { index -> add(tags[index]) }
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): TagViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_tag, parent, false)
return TagViewHolder(view, this)
}
override fun getItemCount() = tags.size
override fun onBindViewHolder(
holder: TagViewHolder,
position: Int
) {
holder.bind(tags[position], checked.contains(position))
}
/** @author Aidan Follestad (@afollestad) */
class TagViewHolder(
itemView: View,
private val adapter: TagAdapter
) : ViewHolder(itemView), OnClickListener {
override fun onClick(v: View) = adapter.toggleChecked(adapterPosition)
init {
itemView.setOnClickListener(this)
}
fun bind(
name: String,
checked: Boolean
) = itemView.chip.run {
text = name
setTextColor(
if (checked) {
WHITE
} else {
ContextCompat.getColor(itemView.context, R.color.unchecked_chip_text)
}
)
setBackgroundResource(
if (checked) {
R.drawable.checked_chip_selector
} else {
R.drawable.unchecked_chip_selector
}
)
}
}
}

View file

@ -28,6 +28,7 @@ import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_addsite.doneBtn import kotlinx.android.synthetic.main.activity_addsite.doneBtn
import kotlinx.android.synthetic.main.activity_addsite.inputName import kotlinx.android.synthetic.main.activity_addsite.inputName
import kotlinx.android.synthetic.main.activity_addsite.inputTags
import kotlinx.android.synthetic.main.activity_addsite.inputUrl import kotlinx.android.synthetic.main.activity_addsite.inputUrl
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
@ -62,6 +63,9 @@ class AddSiteActivity : DarkModeSwitchActivity() {
viewModel.onNameError() viewModel.onNameError()
.toViewError(this, inputName) .toViewError(this, inputName)
// Tags
inputTags.attachLiveData(this, viewModel.tags)
// Url // Url
inputUrl.attachLiveData(this, viewModel.url) inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError() viewModel.onUrlError()

View file

@ -56,6 +56,7 @@ class AddSiteViewModel(
// Public properties // Public properties
val name = MutableLiveData<String>() val name = MutableLiveData<String>()
val tags = MutableLiveData<String>()
val url = MutableLiveData<String>() val url = MutableLiveData<String>()
val timeout = MutableLiveData<Int>() val timeout = MutableLiveData<Int>()
val validationMode = MutableLiveData<ValidationMode>() val validationMode = MutableLiveData<ValidationMode>()
@ -74,6 +75,7 @@ class AddSiteViewModel(
checkIntervalUnit.value = MINUTE checkIntervalUnit.value = MINUTE
retryPolicyMinutes.value = 0 retryPolicyMinutes.value = 0
retryPolicyMinutes.value = 0 retryPolicyMinutes.value = 0
tags.value = ""
} }
// Private properties // Private properties
@ -224,6 +226,8 @@ class AddSiteViewModel(
return null return null
} }
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
val newSettings = SiteSettings( val newSettings = SiteSettings(
validationIntervalMs = getCheckIntervalMs(), validationIntervalMs = getCheckIntervalMs(),
validationMode = validationMode.value!!, validationMode = validationMode.value!!,
@ -252,6 +256,7 @@ class AddSiteViewModel(
id = 0, id = 0,
name = name.value!!.trim(), name = name.value!!.trim(),
url = url.value!!.trim(), url = url.value!!.trim(),
tags = cleanedTags,
settings = newSettings, settings = newSettings,
lastResult = newLastResult, lastResult = newLastResult,
retryPolicy = newRetryPolicy retryPolicy = newRetryPolicy

View file

@ -21,11 +21,13 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.SiteAdapter import com.afollestad.nocknock.adapter.SiteAdapter
import com.afollestad.nocknock.adapter.TagAdapter
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.dialogs.AboutDialog import com.afollestad.nocknock.dialogs.AboutDialog
@ -43,6 +45,7 @@ import kotlinx.android.synthetic.main.include_app_bar.toolbar
import kotlinx.android.synthetic.main.include_empty_view.emptyText import kotlinx.android.synthetic.main.include_empty_view.emptyText
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class MainActivity : DarkModeSwitchActivity() { class MainActivity : DarkModeSwitchActivity() {
@ -53,6 +56,7 @@ class MainActivity : DarkModeSwitchActivity() {
internal val viewModel by viewModel<MainViewModel>() internal val viewModel by viewModel<MainViewModel>()
private lateinit var siteAdapter: SiteAdapter private lateinit var siteAdapter: SiteAdapter
private lateinit var tagAdapter: TagAdapter
private val statusUpdateReceiver by lazy { private val statusUpdateReceiver by lazy {
StatusUpdateIntentReceiver(application, intentProvider) { StatusUpdateIntentReceiver(application, intentProvider) {
@ -76,6 +80,10 @@ class MainActivity : DarkModeSwitchActivity() {
.observe(this, Observer { siteAdapter.set(it) }) .observe(this, Observer { siteAdapter.set(it) })
viewModel.onEmptyTextVisibility() viewModel.onEmptyTextVisibility()
.toViewVisibility(this, emptyText) .toViewVisibility(this, emptyText)
viewModel.onTags()
.observe(this, Observer { tagAdapter.set(it) })
viewModel.onTagsListVisibility()
.toViewVisibility(this, tagsList)
loadingProgress.observe(this, viewModel.onIsLoading()) loadingProgress.observe(this, viewModel.onIsLoading())
processIntent(intent) processIntent(intent)
@ -97,12 +105,18 @@ class MainActivity : DarkModeSwitchActivity() {
} }
siteAdapter = SiteAdapter(this::onSiteSelected) siteAdapter = SiteAdapter(this::onSiteSelected)
list.run { list.run {
layoutManager = LinearLayoutManager(this@MainActivity) layoutManager = LinearLayoutManager(this@MainActivity)
adapter = siteAdapter adapter = siteAdapter
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL)) addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
} }
tagAdapter = TagAdapter(viewModel::onTagSelection)
tagsList.run {
layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
adapter = tagAdapter
}
fab.setOnClickListener { addSite() } fab.setOnClickListener { addSite() }
} }

View file

@ -44,6 +44,8 @@ class MainViewModel(
private val sites = MutableLiveData<List<Site>>() private val sites = MutableLiveData<List<Site>>()
private val isLoading = MutableLiveData<Boolean>() private val isLoading = MutableLiveData<Boolean>()
private val emptyTextVisibility = MutableLiveData<Boolean>() private val emptyTextVisibility = MutableLiveData<Boolean>()
private val tags = MutableLiveData<List<String>>()
private val tagsListVisibility = MutableLiveData<Boolean>()
@CheckResult fun onSites(): LiveData<List<Site>> = sites @CheckResult fun onSites(): LiveData<List<Site>> = sites
@ -51,8 +53,14 @@ class MainViewModel(
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility @CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
@CheckResult fun onTags(): LiveData<List<String>> = tags
@CheckResult fun onTagsListVisibility(): LiveData<Boolean> = tagsListVisibility
@OnLifecycleEvent(ON_RESUME) @OnLifecycleEvent(ON_RESUME)
fun onResume() = loadSites() fun onResume() = loadSites(emptyList())
fun onTagSelection(tags: List<String>) = loadSites(tags)
fun postSiteUpdate(model: Site) { fun postSiteUpdate(model: Site) {
val currentSites = sites.value ?: return val currentSites = sites.value ?: return
@ -94,21 +102,26 @@ class MainViewModel(
} }
} }
private fun loadSites() { private fun loadSites(forTags: List<String>) {
scope.launch { scope.launch {
notificationManager.cancelStatusNotifications() notificationManager.cancelStatusNotifications()
sites.value = listOf()
emptyTextVisibility.value = false emptyTextVisibility.value = false
isLoading.value = true isLoading.value = true
val result = withContext(ioDispatcher) { val result = withContext(ioDispatcher) {
database.allSites() database.allSites(forTags)
} }
sites.value = result sites.value = result
ensureCheckJobs() ensureCheckJobs()
isLoading.value = false isLoading.value = false
emptyTextVisibility.value = result.isEmpty() emptyTextVisibility.value = result.isEmpty()
if (forTags.isEmpty()) {
val tagsValues = pullOutTags(result)
tags.value = tagsValues
tagsListVisibility.value = tagsValues.isNotEmpty()
}
} }
} }
@ -117,4 +130,21 @@ class MainViewModel(
validationManager.ensureScheduledChecks() validationManager.ensureScheduledChecks()
} }
} }
private fun pullOutTags(sites: List<Site>): List<String> {
return mutableListOf<String>().apply {
for (site in sites) {
val splitTags = site.tags.toLowerCase()
.split(',')
splitTags
.filter { it.isNotEmpty() }
.forEach { tag ->
if (!this.contains(tag)) {
this.add(tag)
}
}
}
sort()
}
}
} }

View file

@ -37,6 +37,7 @@ import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
import kotlinx.android.synthetic.main.activity_viewsite.inputName import kotlinx.android.synthetic.main.activity_viewsite.inputName
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
@ -95,6 +96,9 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
viewModel.onNameError() viewModel.onNameError()
.toViewError(this, inputName) .toViewError(this, inputName)
// Tags
inputTags.attachLiveData(this, viewModel.tags)
// Url // Url
inputUrl.attachLiveData(this, viewModel.url) inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError() viewModel.onUrlError()

View file

@ -64,6 +64,7 @@ class ViewSiteViewModel(
// Public properties // Public properties
val status = MutableLiveData<Status>() val status = MutableLiveData<Status>()
val name = MutableLiveData<String>() val name = MutableLiveData<String>()
val tags = MutableLiveData<String>()
val url = MutableLiveData<String>() val url = MutableLiveData<String>()
val timeout = MutableLiveData<Int>() val timeout = MutableLiveData<Int>()
val validationMode = MutableLiveData<ValidationMode>() val validationMode = MutableLiveData<ValidationMode>()
@ -305,6 +306,8 @@ class ViewSiteViewModel(
return null return null
} }
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
val newSettings = site.settings!!.copy( val newSettings = site.settings!!.copy(
validationIntervalMs = getCheckIntervalMs(), validationIntervalMs = getCheckIntervalMs(),
validationMode = validationMode.value!!, validationMode = validationMode.value!!,
@ -332,6 +335,7 @@ class ViewSiteViewModel(
return site.copy( return site.copy(
name = name.value!!.trim(), name = name.value!!.trim(),
tags = cleanedTags,
url = url.value!!.trim(), url = url.value!!.trim(),
settings = newSettings, settings = newSettings,
retryPolicy = retryPolicy retryPolicy = retryPolicy

View file

@ -32,6 +32,7 @@ fun ViewSiteViewModel.setModel(site: Site) {
status.value = site.lastResult?.status ?: WAITING status.value = site.lastResult?.status ?: WAITING
name.value = site.name name.value = site.name
tags.value = site.tags
url.value = site.url url.value = site.url
timeout.value = settings.networkTimeout timeout.value = settings.networkTimeout

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorAccent" android:state_pressed="false"/>
<item android:color="#FFFFFF" android:state_pressed="true"/>
</selector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorAccent"/>
<stroke
android:color="@color/colorAccent_pressed"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent_pressed"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checked_chip" android:state_pressed="false"/>
<item android:drawable="@drawable/checked_chip_pressed" android:state_pressed="true"/>
</selector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?android:windowBackground"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent_translucent"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/unchecked_chip" android:state_pressed="false"/>
<item android:drawable="@drawable/unchecked_chip_pressed" android:state_pressed="true"/>
</selector>

View file

@ -49,6 +49,27 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tagsTiLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:layout_marginTop="@dimen/content_inset_less"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputTags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
android:hint="@string/site_tags_hint"
android:inputType="textPersonName|textCapWords|textAutoCorrect"
style="@style/NockText.Body"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlTiLayout" android:id="@+id/urlTiLayout"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -17,6 +17,19 @@
<include layout="@layout/include_app_bar"/> <include layout="@layout/include_app_bar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="@dimen/content_inset_half"
android:paddingEnd="@dimen/content_inset"
android:paddingStart="@dimen/content_inset"
android:paddingTop="@dimen/content_inset_half"
android:scrollbars="none"
android:visibility="gone"
/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list" android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -67,6 +67,18 @@
style="@style/NockText.Body" style="@style/NockText.Body"
/> />
<EditText
android:id="@+id/inputTags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
android:hint="@string/site_tags_hint"
android:inputType="text|textCapWords"
android:singleLine="true"
tools:ignore="Autofill,UnusedAttribute"
style="@style/NockText.Body"
/>
<EditText <EditText
android:id="@+id/inputUrl" android:id="@+id/inputUrl"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -123,9 +135,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="-4dp" android:layout_marginEnd="-4dp"
android:layout_marginStart="-4dp" android:layout_marginStart="-4dp"
android:layout_marginTop="@dimen/content_inset_quarter"
android:hint="@string/response_timeout_default" android:hint="@string/response_timeout_default"
android:inputType="number" android:inputType="number"
android:layout_marginTop="@dimen/content_inset_quarter"
android:maxLength="8" android:maxLength="8"
tools:ignore="Autofill,HardcodedText,LabelFor" tools:ignore="Autofill,HardcodedText,LabelFor"
style="@style/NockText.Body" style="@style/NockText.Body"

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_inset_half"
android:background="@drawable/unchecked_chip_selector"
android:textColor="?colorAccent"
app:textAllCaps="true"
tools:text="Testing"
style="@style/NockText.Body"
/>

View file

@ -9,5 +9,7 @@
<color name="lighterGray">#303030</color> <color name="lighterGray">#303030</color>
<color name="colorAccent">#FF6E40</color> <color name="colorAccent">#FF6E40</color>
<color name="colorAccent_pressed">#E44615</color>
<color name="colorAccent_translucent">#40FF6E40</color>
</resources> </resources>

View file

@ -21,6 +21,8 @@
<string name="add_site">Add Site</string> <string name="add_site">Add Site</string>
<string name="site_name">Site Name</string> <string name="site_name">Site Name</string>
<string name="site_url">Site URL</string> <string name="site_url">Site URL</string>
<string name="site_tags">Site Tags</string>
<string name="site_tags_hint">Site Tags (one, two, three)</string>
<string name="please_enter_name">Please enter a name!</string> <string name="please_enter_name">Please enter a name!</string>
<string name="please_enter_url">Please enter a URL.</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_valid_url">Please enter a valid URL.</string>

View file

@ -32,7 +32,7 @@ import com.afollestad.nocknock.data.model.ValidationResult
SiteSettings::class, SiteSettings::class,
Site::class Site::class
], ],
version = 2, version = 3,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@ -52,13 +52,13 @@ abstract class AppDatabase : RoomDatabase() {
* *
* @author Aidan Follestad (@afollestad) * @author Aidan Follestad (@afollestad)
*/ */
fun AppDatabase.allSites(forTag: String = ""): List<Site> { fun AppDatabase.allSites(tags: List<String> = emptyList()): List<Site> {
val lowercaseTag = forTag.toLowerCase()
var all = siteDao().all() var all = siteDao().all()
if (!forTag.isEmpty()) { if (tags.isNotEmpty()) {
all = all.filter { all = all.filter { site ->
forTag.isEmpty() || val itemTags = site.tags.toLowerCase()
it.tags.toLowerCase().split(",").contains(lowercaseTag) .split(",")
return@filter itemTags.any { tag -> tags.contains(tag) }
} }
} }
return all.map { return all.map {

View file

@ -27,7 +27,7 @@ class Database1to2Migration : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL( database.execSQL(
"CREATE TABLE `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL)" "CREATE TABLE `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL, lastTryTimestamp INTEGER NOT NULL, triesLeft INTEGER NOT NULL)"
) )
} }
} }
@ -37,9 +37,9 @@ class Database1to2Migration : Migration(1, 2) {
* *
* @author Aidan Follestad (@afollestad) * @author Aidan Follestad (@afollestad)
*/ */
class Database2to3Migration : Migration(1, 2) { class Database2to3Migration : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL") database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL DEFAULT ''")
} }
} }

View file

@ -15,7 +15,6 @@ ext.versions = [
// Misc // Misc
okHttp : '3.12.1', okHttp : '3.12.1',
rhino : '1.7.10', rhino : '1.7.10',
chipView : '1.0.3',
// Kotlin // Kotlin
kotlin : '1.3.11', kotlin : '1.3.11',

View file

@ -225,6 +225,7 @@ class ValidationJob : JobService() {
triesLeft: Int triesLeft: Int
) { ) {
retryPolicy.triesLeft = triesLeft retryPolicy.triesLeft = triesLeft
retryPolicy.lastTryTimestamp = currentTimeMillis()
withContext(IO) { withContext(IO) {
database.retryPolicyDao() database.retryPolicyDao()
.update(retryPolicy) .update(retryPolicy)