Material Design 2 esque UI and a dark mode. Resolves #37, resolves #38.

This commit is contained in:
Aidan Follestad 2019-01-06 21:29:07 -08:00
commit de36a2f5e6
32 changed files with 517 additions and 115 deletions

View file

@ -27,6 +27,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
implementation 'com.google.android.material:material:' + versions.googleMaterial implementation 'com.google.android.material:material:' + versions.googleMaterial
implementation 'androidx.browser:browser:' + versions.androidxBrowser
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle

View file

@ -18,9 +18,18 @@ package com.afollestad.nocknock
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks import android.app.Application.ActivityLifecycleCallbacks
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.annotation.AttrRes
import androidx.annotation.ColorRes
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.core.text.HtmlCompat.fromHtml import androidx.core.text.HtmlCompat.fromHtml
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
import com.afollestad.nocknock.utilities.ui.toast
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
@ -50,3 +59,53 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
} }
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY) fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
fun String.toUri() = Uri.parse(this)!!
fun Activity.setStatusBarColor(
@ColorRes res: Int? = null,
@AttrRes attr: Int? = null
) {
require(res != null || attr != null) { "Must specify at least one arg." }
if (res != null) {
val color = ContextCompat.getColor(this, res)
window.statusBarColor = color
} else if (attr != null) {
val color = resolveColor(this, attr = attr)
window.statusBarColor = color
}
}
fun Activity.viewUrl(url: String) {
val customTabsIntent = CustomTabsIntent.Builder()
.apply {
setToolbarColor(resolveColor(this@viewUrl, attr = R.attr.colorPrimary))
}
.build()
try {
customTabsIntent.launchUrl(this, url.toUri())
} catch (_: ActivityNotFoundException) {
toast(R.string.install_web_browser)
}
}
fun Activity.viewUrlWithApp(
url: String,
pkg: String
) {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = url.toUri()
}
val resInfo = packageManager.queryIntentActivities(intent, 0)
for (info in resInfo) {
if (info.activityInfo.packageName.toLowerCase().contains(pkg) ||
info.activityInfo.name.toLowerCase().contains(pkg)
) {
startActivity(intent.apply {
setPackage(info.activityInfo.packageName)
})
return
}
}
viewUrl(url)
}

View file

@ -20,6 +20,7 @@ package com.afollestad.nocknock
import android.app.Application import android.app.Application
import com.afollestad.nocknock.engine.engineModule import com.afollestad.nocknock.engine.engineModule
import com.afollestad.nocknock.koin.mainModule import com.afollestad.nocknock.koin.mainModule
import com.afollestad.nocknock.koin.prefModule
import com.afollestad.nocknock.koin.viewModelModule import com.afollestad.nocknock.koin.viewModelModule
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.notifications.notificationsModule import com.afollestad.nocknock.notifications.notificationsModule
@ -42,6 +43,7 @@ class NockNockApp : Application() {
} }
val modules = listOf( val modules = listOf(
prefModule,
mainModule, mainModule,
engineModule, engineModule,
commonModule, commonModule,

View file

@ -37,7 +37,7 @@ typealias Listener = (model: Site, longClick: Boolean) -> Unit
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class SiteViewHolder constructor( class SiteViewHolder constructor(
itemView: View, itemView: View,
private val adapter: ServerAdapter private val adapter: SiteAdapter
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener { ) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
init { init {
@ -94,7 +94,7 @@ class SiteViewHolder constructor(
} }
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<SiteViewHolder>() { class SiteAdapter(private val listener: Listener) : RecyclerView.Adapter<SiteViewHolder>() {
private var models = mutableListOf<Site>() private var models = mutableListOf<Site>()

View file

@ -37,7 +37,7 @@ class StatusUpdateIntentReceiver(
private var callback: SiteCallback? private var callback: SiteCallback?
) : LifecycleObserver { ) : LifecycleObserver {
internal val intentReceiver = object : BroadcastReceiver() { private val intentReceiver = object : BroadcastReceiver() {
override fun onReceive( override fun onReceive(
context: Context, context: Context,
intent: Intent intent: Intent

View file

@ -0,0 +1,32 @@
/**
* 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.koin
import com.afollestad.rxkprefs.RxkPrefs
import com.afollestad.rxkprefs.rxkPrefs
import org.koin.dsl.module.module
const val PREF_DARK_MODE = "dark_mode"
/** @author Aidan Follestad (@afollestad) */
val prefModule = module {
single { rxkPrefs(get(), "settings") }
factory(name = PREF_DARK_MODE) {
get<RxkPrefs>().boolean(PREF_DARK_MODE, false)
}
}

View file

@ -0,0 +1,56 @@
/**
* 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.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R
import com.afollestad.nocknock.koin.PREF_DARK_MODE
import com.afollestad.nocknock.utilities.rx.attachLifecycle
import com.afollestad.rxkprefs.Pref
import org.koin.android.ext.android.inject
import timber.log.Timber.d as log
/** @author Aidan Follestad (afollestad) */
abstract class DarkModeSwitchActivity : AppCompatActivity() {
private var isDarkModeEnabled: Boolean = false
private val darkModePref by inject<Pref<Boolean>>(name = PREF_DARK_MODE)
override fun onCreate(savedInstanceState: Bundle?) {
isDarkModeEnabled = darkModePref.get()
setTheme(themeRes())
super.onCreate(savedInstanceState)
darkModePref.observe()
.filter { it != isDarkModeEnabled }
.subscribe {
log("Theme changed, recreating Activity.")
recreate()
}
.attachLifecycle(this)
}
override fun onResume() {
super.onResume()
}
private fun themeRes() = if (darkModePref.get()) {
R.style.AppTheme_Dark
} else {
R.style.AppTheme
}
}

View file

@ -21,6 +21,7 @@ import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.setStatusBarColor
import com.afollestad.nocknock.viewcomponents.ext.conceal import com.afollestad.nocknock.viewcomponents.ext.conceal
import com.afollestad.nocknock.viewcomponents.ext.onLayout import com.afollestad.nocknock.viewcomponents.ext.onLayout
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
@ -38,11 +39,12 @@ import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchT
import kotlinx.android.synthetic.main.activity_addsite.rootView import kotlinx.android.synthetic.main.activity_addsite.rootView
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
import kotlinx.android.synthetic.main.activity_addsite.toolbar
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
import kotlinx.android.synthetic.main.include_app_bar.toolbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.max import kotlin.math.max
import kotlin.properties.Delegates.notNull import kotlin.properties.Delegates.notNull
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
const val KEY_FAB_X = "fab_x" const val KEY_FAB_X = "fab_x"
const val KEY_FAB_Y = "fab_y" const val KEY_FAB_Y = "fab_y"
@ -62,6 +64,7 @@ class AddSiteActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStatusBarColor(res = R.color.inkColorDark)
setContentView(R.layout.activity_addsite) setContentView(R.layout.activity_addsite)
setupUi(savedInstanceState) setupUi(savedInstanceState)
@ -133,7 +136,11 @@ class AddSiteActivity : AppCompatActivity() {
} }
private fun setupUi(savedInstanceState: Bundle?) { private fun setupUi(savedInstanceState: Bundle?) {
toolbar.setNavigationOnClickListener { closeActivityWithReveal() } toolbarTitle.setText(R.string.add_site)
toolbar.run {
setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { closeActivityWithReveal() }
}
if (savedInstanceState == null) { if (savedInstanceState == null) {
rootView.conceal() rootView.conceal()

View file

@ -18,6 +18,8 @@ package com.afollestad.nocknock.ui.addsite
import android.view.ViewAnimationUtils.createCircularReveal import android.view.ViewAnimationUtils.createCircularReveal
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import com.afollestad.nocknock.R
import com.afollestad.nocknock.setStatusBarColor
import com.afollestad.nocknock.utilities.ext.onEnd import com.afollestad.nocknock.utilities.ext.onEnd
import com.afollestad.nocknock.viewcomponents.ext.conceal import com.afollestad.nocknock.viewcomponents.ext.conceal
import com.afollestad.nocknock.viewcomponents.ext.show import com.afollestad.nocknock.viewcomponents.ext.show
@ -40,6 +42,7 @@ internal fun AddSiteActivity.closeActivityWithReveal() {
if (isClosing) { if (isClosing) {
return return
} }
setStatusBarColor(attr = R.attr.colorPrimary)
isClosing = true isClosing = true
createCircularReveal(rootView, revealCx, revealCy, revealRadius, 0f) createCircularReveal(rootView, revealCx, revealCy, revealRadius, 0f)
.apply { .apply {

View file

@ -17,38 +17,45 @@ package com.afollestad.nocknock.ui.main
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer 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 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.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.ServerAdapter import com.afollestad.nocknock.adapter.SiteAdapter
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
import com.afollestad.nocknock.koin.PREF_DARK_MODE
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.ui.toast
import com.afollestad.nocknock.viewUrl
import com.afollestad.nocknock.viewUrlWithApp
import com.afollestad.nocknock.viewcomponents.ext.showOrHide import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import com.afollestad.rxkprefs.Pref
import kotlinx.android.synthetic.main.activity_main.fab import kotlinx.android.synthetic.main.activity_main.fab
import kotlinx.android.synthetic.main.activity_main.list import kotlinx.android.synthetic.main.activity_main.list
import kotlinx.android.synthetic.main.activity_main.loadingProgress import kotlinx.android.synthetic.main.activity_main.loadingProgress
import kotlinx.android.synthetic.main.activity_main.toolbar 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
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class MainActivity : AppCompatActivity() { class MainActivity : DarkModeSwitchActivity() {
private val notificationManager by inject<NockNotificationManager>() private val notificationManager by inject<NockNotificationManager>()
private val intentProvider by inject<IntentProvider>() private val intentProvider by inject<IntentProvider>()
private val darkModePref by inject<Pref<Boolean>>(name = PREF_DARK_MODE)
internal val viewModel by viewModel<MainViewModel>() internal val viewModel by viewModel<MainViewModel>()
private lateinit var adapter: ServerAdapter private lateinit var siteAdapter: SiteAdapter
private val statusUpdateReceiver by lazy { private val statusUpdateReceiver by lazy {
StatusUpdateIntentReceiver(application, intentProvider) { StatusUpdateIntentReceiver(application, intentProvider) {
@ -70,7 +77,7 @@ class MainActivity : AppCompatActivity() {
viewModel.onSites() viewModel.onSites()
.observe(this, Observer { .observe(this, Observer {
adapter.set(it) siteAdapter.set(it)
emptyText.showOrHide(it.isEmpty()) emptyText.showOrHide(it.isEmpty())
}) })
loadingProgress.observe(this, viewModel.onIsLoading()) loadingProgress.observe(this, viewModel.onIsLoading())
@ -79,20 +86,27 @@ class MainActivity : AppCompatActivity() {
} }
private fun setupUi() { private fun setupUi() {
toolbar.inflateMenu(R.menu.menu_main) toolbar.run {
toolbar.setOnMenuItemClickListener { item -> inflateMenu(R.menu.menu_main)
if (item.itemId == R.id.about) { menu.findItem(R.id.dark_mode)
AboutDialog.show(this) .isChecked = darkModePref.get()
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.about -> AboutDialog.show(this@MainActivity)
R.id.dark_mode -> darkModePref.set(!darkModePref.get())
R.id.support_me -> supportMe()
} }
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
}
adapter = ServerAdapter(this::onSiteSelected) siteAdapter = SiteAdapter(this::onSiteSelected)
list.layoutManager = LinearLayoutManager(this)
list.adapter = adapter
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
list.run {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = siteAdapter
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
}
fab.setOnClickListener { addSite() } fab.setOnClickListener { addSite() }
} }
@ -119,4 +133,20 @@ class MainActivity : AppCompatActivity() {
viewSite(model) viewSite(model)
} }
} }
private fun supportMe() {
MaterialDialog(this).show {
title(R.string.support_me)
message(R.string.support_me_message, html = true, lineHeightMultiplier = 1.4f)
listItemsSingleChoice(R.array.donation_options) { _, index, _ ->
when (index) {
0 -> viewUrl("https://paypal.me/AidanFollestad")
1 -> viewUrlWithApp("https://cash.me/\$afollestad", pkg = "com.squareup.cash")
2 -> viewUrlWithApp("https://venmo.com/afollestad", pkg = "com.venmo")
}
toast(R.string.thank_you)
}
positiveButton(R.string.next)
}
}
} }

View file

@ -25,10 +25,11 @@ import com.afollestad.nocknock.R
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.data.model.ValidationMode import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.setStatusBarColor
import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.onScroll import com.afollestad.nocknock.viewcomponents.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.toViewError import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewText import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
@ -47,10 +48,11 @@ import kotlinx.android.synthetic.main.activity_viewsite.scrollView
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning 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.activity_viewsite.validationModeDescription
import kotlinx.android.synthetic.main.include_app_bar.toolbar
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.include_app_bar.toolbar_title as toolbarTitle
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class ViewSiteActivity : AppCompatActivity() { class ViewSiteActivity : AppCompatActivity() {
@ -67,6 +69,7 @@ class ViewSiteActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStatusBarColor(res = R.color.inkColorDark)
setContentView(R.layout.activity_viewsite) setContentView(R.layout.activity_viewsite)
setupUi() setupUi()
@ -156,7 +159,9 @@ class ViewSiteActivity : AppCompatActivity() {
} }
private fun setupUi() { private fun setupUi() {
toolbarTitle.setText(R.string.add_site)
toolbar.run { toolbar.run {
setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { finish() } setNavigationOnClickListener { finish() }
inflateMenu(R.menu.menu_viewsite) inflateMenu(R.menu.menu_viewsite)
menu.findItem(R.id.refresh) menu.findItem(R.id.refresh)

View file

@ -22,7 +22,7 @@ import com.afollestad.nocknock.data.model.Status
import com.afollestad.nocknock.data.model.isPending import com.afollestad.nocknock.data.model.isPending
import com.afollestad.nocknock.toHtml import com.afollestad.nocknock.toHtml
import com.afollestad.nocknock.utilities.ext.animateRotation import com.afollestad.nocknock.utilities.ext.animateRotation
import kotlinx.android.synthetic.main.activity_viewsite.toolbar import kotlinx.android.synthetic.main.include_app_bar.toolbar
const val KEY_SITE = "site_model" const val KEY_SITE = "site_model"

View file

@ -1,4 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp"/> <size android:height="1dp"/>
<solid android:color="@color/dividerColor"/> <solid android:color="?dividerColor"/>
</shape> </shape>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -15,15 +14,7 @@
android:orientation="vertical" android:orientation="vertical"
> >
<androidx.appcompat.widget.Toolbar <include layout="@layout/include_app_bar"/>
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/FlatToolbarTheme"
app:navigationIcon="@drawable/ic_action_close"
app:title="@string/add_site"
app:titleTextColor="#FFFFFF"
/>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -86,8 +77,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_text_spacing" android:layout_marginTop="@dimen/list_text_spacing"
android:visibility="gone"
android:text="@string/warning_http_url" android:text="@string/warning_http_url"
android:visibility="gone"
style="@style/NockText.Footnote" style="@style/NockText.Footnote"
/> />
@ -157,7 +148,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset" android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half" android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark" android:background="@color/inkColorDark"
/> />
<TextView <TextView

View file

@ -15,13 +15,7 @@
android:orientation="vertical" android:orientation="vertical"
> >
<androidx.appcompat.widget.Toolbar <include layout="@layout/include_app_bar"/>
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/MainToolbarTheme"
style="@style/MainToolbarStyle"
/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list" android:id="@+id/list"
@ -34,18 +28,23 @@
<include layout="@layout/include_empty_view"/> <include layout="@layout/include_empty_view"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:layout_margin="@dimen/content_inset" android:layout_marginBottom="@dimen/content_inset"
android:src="@drawable/ic_add" android:layout_marginEnd="@dimen/content_inset_more"
app:backgroundTint="?colorAccent" android:minHeight="64dp"
app:elevation="@dimen/fab_elevation" android:paddingBottom="@dimen/content_inset_half"
app:fabSize="normal" android:paddingEnd="@dimen/content_inset"
app:pressedTranslationZ="@dimen/fab_elevation_pressed" android:paddingStart="@dimen/content_inset"
app:rippleColor="#40ffffff" android:paddingTop="@dimen/content_inset_half"
android:text="@string/add_site"
app:cornerRadius="32dp"
app:icon="@drawable/ic_add"
app:iconTint="#fff"
style="@style/Widget.MaterialComponents.Button.Icon"
/> />
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame <com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -15,17 +14,7 @@
android:orientation="vertical" android:orientation="vertical"
> >
<!-- Background is applied again here so programmatic elevation works --> <include layout="@layout/include_app_bar"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:theme="@style/FlatToolbarTheme"
app:navigationIcon="@drawable/ic_action_close"
app:title="@string/view_site"
app:titleTextColor="?android:textColorPrimary"
/>
<ScrollView <ScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
@ -187,7 +176,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset" android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half" android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark" android:background="@color/inkColorDark"
/> />
<TextView <TextView

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:elevation="0dp"
android:gravity="center"
tools:ignore="Overdraw"
>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:elevation="0dp"
/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/toolbar_title"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:gravity="center"
android:text="@string/app_name"
android:textAppearance="@style/AppTheme.TextAppearance.Title"
/>
</FrameLayout>

View file

@ -3,4 +3,11 @@
<item <item
android:id="@+id/about" android:id="@+id/about"
android:title="@string/about"/> android:title="@string/about"/>
<item
android:id="@+id/dark_mode"
android:checkable="true"
android:title="@string/dark_mode"/>
<item
android:id="@+id/support_me"
android:title="@string/support_me"/>
</menu> </menu>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppThemeParent.Ink" parent="AppThemeParent.Dark">
<item name="android:navigationBarColor">@color/inkColorDark</item>
</style>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="AppThemeParent">
<item name="android:statusBarColor">@color/colorPrimary_lightTheme</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="AppThemeParent">
<item name="android:statusBarColor">@color/colorPrimary_lightTheme</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark_lightTheme</item>
<item name="android:windowLightNavigationBar">true</item>
</style>
<style name="AppTheme.Dark" parent="AppThemeParent.Dark">
<item name="android:statusBarColor">@color/colorPrimary_darkTheme</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark_darkTheme</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>

View file

@ -12,4 +12,10 @@
<item>JavaScript Evaluation</item> <item>JavaScript Evaluation</item>
</string-array> </string-array>
<string-array name="donation_options">
<item>via PayPal</item>
<item>via Cash App</item>
<item>via Venmo</item>
</string-array>
</resources> </resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr format="color" name="toolbarTitleColor"/>
<attr format="color" name="dividerColor"/>
<attr format="color" name="iconColor"/>
</resources>

View file

@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#455A64</color> <color name="colorPrimary_lightTheme">#FFFFFF</color>
<color name="colorPrimaryDark">#37474F</color> <color name="colorPrimaryDark_lightTheme">#F5F5F5</color>
<color name="colorPrimary_darkTheme">#212121</color>
<color name="colorPrimaryDark_darkTheme">#212121</color>
<color name="inkColor">#455A64</color>
<color name="inkColorDark">#37474F</color>
<color name="colorAccent">#FF6E40</color> <color name="colorAccent">#FF6E40</color>
<color name="dividerColor">#EEEEEE</color>
</resources> </resources>

View file

@ -14,6 +14,7 @@
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i> <br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>. <br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
]]></string> ]]></string>
<string name="dark_mode">Dark Mode</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="add_site">Add Site</string> <string name="add_site">Add Site</string>
@ -74,4 +75,15 @@
exception to pass custom error messages to Nock Nock. exception to pass custom error messages to Nock Nock.
</string> </string>
<string name="support_me">Donate</string>
<string name="support_me_message"><![CDATA[
<b>MNML</b> was created and is maintained by one person. Donations are <b>much</b>
appreciated and encourage continued support.
]]></string>
<string name="thank_you">Thank you very much!</string>
<string name="next">Next</string>
<string name="install_video_viewer">Please install a video viewer app, such as Google Photos.</string>
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
</resources> </resources>

View file

@ -1,27 +1,14 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <style name="AppTheme" parent="AppThemeParent"/>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
<style name="AppTheme.Ink" parent="AppThemeParent.Ink">
<item name="colorPrimary">@color/inkColor</item>
<item name="colorPrimaryDark">@color/inkColorDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="colorButtonNormal">@color/colorPrimaryDark</item> <item name="colorButtonNormal">@color/inkColor</item>
<item name="android:listDivider">@drawable/divider</item>
<item name="android:textColorPrimary">#212121</item>
<item name="android:textColorSecondary">#727272</item>
<item name="md_corner_radius">16dp</item>
<item name="md_font_title">@font/lato_black</item>
<item name="md_font_body">@font/lato</item>
<item name="md_font_button">@font/lato_bold</item>
</style>
<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>
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
<item name="android:listDivider">@drawable/divider</item> <item name="android:listDivider">@drawable/divider</item>
@ -37,28 +24,6 @@
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
</style> </style>
<style name="MainToolbarTheme" parent="@style/Theme.MaterialComponents">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:fontFamily">@font/lato_black</item>
</style>
<style name="MainToolbarStyle" parent="@style/Widget.MaterialComponents.Toolbar">
<item name="android:background">?colorPrimary</item>
<item name="android:elevation">@dimen/default_elevation</item>
<item name="title">@string/app_name</item>
<item name="titleTextColor">#FFFFFF</item>
<item name="popupTheme">@style/Theme.MaterialComponents.Light.DarkActionBar</item>
</style>
<style name="FlatToolbarTheme" parent="@style/Theme.MaterialComponents">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:fontFamily">@font/lato_black</item>
</style>
<style name="AccentButton" parent="Widget.MaterialComponents.Button"> <style name="AccentButton" parent="Widget.MaterialComponents.Button">
<item name="android:textColor">#fff</item> <item name="android:textColor">#fff</item>
<item name="backgroundTint">@color/colorAccent</item> <item name="backgroundTint">@color/colorAccent</item>
@ -67,7 +32,7 @@
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button"> <style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
<item name="android:textColor">#fff</item> <item name="android:textColor">#fff</item>
<item name="backgroundTint">@color/colorPrimaryDark</item> <item name="backgroundTint">@color/inkColorDark</item>
<item name="android:fontFamily">@font/lato</item> <item name="android:fontFamily">@font/lato</item>
</style> </style>

View file

@ -0,0 +1,48 @@
<resources>
<style name="AppThemeParent" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary_lightTheme</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark_lightTheme</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorButtonNormal">@color/inkColorDark</item>
<item name="toolbarTitleColor">#000000</item>
<item name="dividerColor">#1f000000</item>
<item name="iconColor">#000000</item>
<item name="android:windowBackground">@color/colorPrimary_lightTheme</item>
<item name="android:listDivider">@drawable/divider</item>
<item name="android:statusBarColor">#E5E5E5</item>
<item name="md_corner_radius">16dp</item>
<item name="md_font_title">@font/lato_bold</item>
<item name="md_font_body">@font/lato</item>
<item name="md_font_button">@font/lato_bold</item>
</style>
<style name="AppThemeParent.Dark" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorPrimary">@color/colorPrimary_darkTheme</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark_darkTheme</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorButtonNormal">@color/inkColorDark</item>
<item name="toolbarTitleColor">#ffffff</item>
<item name="dividerColor">#1fffffff</item>
<item name="iconColor">#FFFFFF</item>
<item name="android:windowBackground">@color/colorPrimary_darkTheme</item>
<item name="android:listDivider">@drawable/divider</item>
<item name="android:statusBarColor">@color/colorPrimary_darkTheme</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark_darkTheme</item>
<item name="md_corner_radius">16dp</item>
<item name="md_font_title">@font/lato_bold</item>
<item name="md_font_body">@font/lato</item>
<item name="md_font_button">@font/lato_bold</item>
</style>
<style name="AppThemeParent.Ink" parent="AppThemeParent.Dark"/>
</resources>

View file

@ -0,0 +1,9 @@
<resources>
<style name="AppTheme.TextAppearance.Title" parent="TextAppearance.MaterialComponents.Headline6">
<item name="fontFamily">@font/lato_bold</item>
<item name="android:fontFamily">@font/lato_bold</item>
<item name="android:textColor">?toolbarTitleColor</item>
</style>
</resources>

View file

@ -29,6 +29,8 @@ dependencies {
implementation 'org.koin:koin-android:' + versions.koin implementation 'org.koin:koin-android:' + versions.koin
implementation 'org.mozilla:rhino:' + versions.rhino implementation 'org.mozilla:rhino:' + versions.rhino
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
testImplementation 'junit:junit:' + versions.junit testImplementation 'junit:junit:' + versions.junit
testImplementation 'com.google.truth:truth:' + versions.truth testImplementation 'com.google.truth:truth:' + versions.truth
testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting

View file

@ -0,0 +1,58 @@
/**
* 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.
*/
@file:Suppress("unused")
package com.afollestad.nocknock.utilities.rx
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.lifecycle.Lifecycle.State.DESTROYED
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import io.reactivex.disposables.Disposable
/** @author Aidan Follestad (afollestad) */
class LifecycleAwareDisposable(
private val disposable: Disposable
) : LifecycleObserver {
@OnLifecycleEvent(ON_DESTROY)
fun dispose() = disposable.dispose()
}
/**
* Wraps [disposable] so that it is disposed of when the receiving [LifecycleOwner]
* is destroyed.
*
* @author Aidan Follestad (afollestad)
*/
fun LifecycleOwner.ownRx(disposable: Disposable) {
if (this.lifecycle.currentState == DESTROYED) {
disposable.dispose()
return
}
this.lifecycle.addObserver(LifecycleAwareDisposable(disposable))
}
/**
* Attaches the receiving [Disposable] so that it is disposed of when [lifecycleOwner]
* is destroyed.
*
* @author Aidan Follestad (afollestad)
*/
fun Disposable.attachLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.ownRx(this)
}

View file

@ -0,0 +1,30 @@
/**
* 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.utilities.ui
import android.content.Context
import android.widget.Toast
private var toast: Toast? = null
/** Shows a toast in the receiving context, cancelling any previous. */
fun Context.toast(message: Int) {
toast?.cancel()
toast = Toast.makeText(this, message, Toast.LENGTH_LONG)
.apply {
show()
}
}

View file

@ -1,39 +1,51 @@
ext.versions = [ ext.versions = [
// Project
minSdk : 21, minSdk : 21,
compileSdk : 28, compileSdk : 28,
buildTools : '28.0.3', buildTools : '28.0.3',
publishVersion : '0.8.0', publishVersion : '0.8.0',
publishVersionCode : 28, publishVersionCode : 28,
// Plugins
gradlePlugin : '3.2.1', gradlePlugin : '3.2.1',
spotlessPlugin : '3.17.0', spotlessPlugin : '3.17.0',
versionPlugin : '0.20.0', versionPlugin : '0.20.0',
// Misc
okHttp : '3.12.1', okHttp : '3.12.1',
rhino : '1.7.10', rhino : '1.7.10',
// Kotlin
kotlin : '1.3.11', kotlin : '1.3.11',
coroutines : '1.1.0', coroutines : '1.1.0',
koin : '1.0.2', koin : '1.0.2',
// Google/AndroidX
androidxAnnotations : '1.0.1', androidxAnnotations : '1.0.1',
androidxCore : '1.0.2', androidxCore : '1.0.2',
androidxRecyclerView: '1.0.0', androidxRecyclerView: '1.0.0',
androidxBrowser : '1.0.0',
googleMaterial : '1.0.0', googleMaterial : '1.0.0',
room : '2.0.0', room : '2.0.0',
lifecycle : '2.0.0', lifecycle : '2.0.0',
// Rx
rxBinding : '3.0.0-alpha1', rxBinding : '3.0.0-alpha1',
// afollestad
materialDialogs : '2.0.0-rc7', materialDialogs : '2.0.0-rc7',
rxkPrefs : '1.2.0', rxkPrefs : '1.2.1',
// Debugging
timber : '4.7.1', timber : '4.7.1',
// Unit testing
junit : '4.12', junit : '4.12',
mockito : '2.23.4', mockito : '2.23.4',
mockitoKotlin : '2.0.0-RC1', mockitoKotlin : '2.0.0-RC1',
truth : '0.42', truth : '0.42',
// UI testing
androidxTestRunner : '1.1.1', androidxTestRunner : '1.1.1',
androidxTest : '1.1.0', androidxTest : '1.1.0',
archTesting : '2.0.0' archTesting : '2.0.0'