mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-08-25 11:48:21 +00:00
Tag system, resolves #13
This commit is contained in:
parent
2756fc9fc7
commit
002149cd3f
26 changed files with 326 additions and 20 deletions
|
@ -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}") {
|
||||||
|
|
115
app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt
Normal file
115
app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt
Normal 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
5
app/src/main/res/color/unchecked_chip_text.xml
Normal file
5
app/src/main/res/color/unchecked_chip_text.xml
Normal 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>
|
13
app/src/main/res/drawable/checked_chip.xml
Normal file
13
app/src/main/res/drawable/checked_chip.xml
Normal 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>
|
13
app/src/main/res/drawable/checked_chip_pressed.xml
Normal file
13
app/src/main/res/drawable/checked_chip_pressed.xml
Normal 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>
|
5
app/src/main/res/drawable/checked_chip_selector.xml
Normal file
5
app/src/main/res/drawable/checked_chip_selector.xml
Normal 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>
|
13
app/src/main/res/drawable/unchecked_chip.xml
Normal file
13
app/src/main/res/drawable/unchecked_chip.xml
Normal 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>
|
13
app/src/main/res/drawable/unchecked_chip_pressed.xml
Normal file
13
app/src/main/res/drawable/unchecked_chip_pressed.xml
Normal 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>
|
5
app/src/main/res/drawable/unchecked_chip_selector.xml
Normal file
5
app/src/main/res/drawable/unchecked_chip_selector.xml
Normal 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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
15
app/src/main/res/layout/list_item_tag.xml
Normal file
15
app/src/main/res/layout/list_item_tag.xml
Normal 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"
|
||||||
|
/>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 ''")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue