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.recyclerview:recyclerview:' + versions.androidxRecyclerView
implementation 'com.google.android.material:material:' + versions.googleMaterial
implementation 'androidx.browser:browser:' + versions.androidxBrowser
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle

View file

@ -18,9 +18,18 @@ package com.afollestad.nocknock
import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
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.fromHtml
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
import com.afollestad.nocknock.utilities.ui.toast
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.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 com.afollestad.nocknock.engine.engineModule
import com.afollestad.nocknock.koin.mainModule
import com.afollestad.nocknock.koin.prefModule
import com.afollestad.nocknock.koin.viewModelModule
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.notifications.notificationsModule
@ -42,6 +43,7 @@ class NockNockApp : Application() {
}
val modules = listOf(
prefModule,
mainModule,
engineModule,
commonModule,

View file

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

View file

@ -37,7 +37,7 @@ class StatusUpdateIntentReceiver(
private var callback: SiteCallback?
) : LifecycleObserver {
internal val intentReceiver = object : BroadcastReceiver() {
private val intentReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
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 com.afollestad.nocknock.R
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.onLayout
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.scriptInputLayout
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.include_app_bar.toolbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.max
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_Y = "fab_y"
@ -62,6 +64,7 @@ class AddSiteActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStatusBarColor(res = R.color.inkColorDark)
setContentView(R.layout.activity_addsite)
setupUi(savedInstanceState)
@ -133,7 +136,11 @@ class AddSiteActivity : AppCompatActivity() {
}
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) {
rootView.conceal()

View file

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

View file

@ -17,38 +17,45 @@ package com.afollestad.nocknock.ui.main
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsSingleChoice
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.data.model.Site
import com.afollestad.nocknock.dialogs.AboutDialog
import com.afollestad.nocknock.koin.PREF_DARK_MODE
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
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.rxkprefs.Pref
import kotlinx.android.synthetic.main.activity_main.fab
import kotlinx.android.synthetic.main.activity_main.list
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 org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
/** @author Aidan Follestad (@afollestad) */
class MainActivity : AppCompatActivity() {
class MainActivity : DarkModeSwitchActivity() {
private val notificationManager by inject<NockNotificationManager>()
private val intentProvider by inject<IntentProvider>()
private val darkModePref by inject<Pref<Boolean>>(name = PREF_DARK_MODE)
internal val viewModel by viewModel<MainViewModel>()
private lateinit var adapter: ServerAdapter
private lateinit var siteAdapter: SiteAdapter
private val statusUpdateReceiver by lazy {
StatusUpdateIntentReceiver(application, intentProvider) {
@ -70,7 +77,7 @@ class MainActivity : AppCompatActivity() {
viewModel.onSites()
.observe(this, Observer {
adapter.set(it)
siteAdapter.set(it)
emptyText.showOrHide(it.isEmpty())
})
loadingProgress.observe(this, viewModel.onIsLoading())
@ -79,20 +86,27 @@ class MainActivity : AppCompatActivity() {
}
private fun setupUi() {
toolbar.inflateMenu(R.menu.menu_main)
toolbar.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.about) {
AboutDialog.show(this)
toolbar.run {
inflateMenu(R.menu.menu_main)
menu.findItem(R.id.dark_mode)
.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)
list.layoutManager = LinearLayoutManager(this)
list.adapter = adapter
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
siteAdapter = SiteAdapter(this::onSiteSelected)
list.run {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = siteAdapter
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
}
fab.setOnClickListener { addSite() }
}
@ -119,4 +133,20 @@ class MainActivity : AppCompatActivity() {
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.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.setStatusBarColor
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.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
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.textNextCheck
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
import kotlinx.android.synthetic.main.include_app_bar.toolbar
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
/** @author Aidan Follestad (@afollestad) */
class ViewSiteActivity : AppCompatActivity() {
@ -67,6 +69,7 @@ class ViewSiteActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStatusBarColor(res = R.color.inkColorDark)
setContentView(R.layout.activity_viewsite)
setupUi()
@ -156,7 +159,9 @@ class ViewSiteActivity : AppCompatActivity() {
}
private fun setupUi() {
toolbarTitle.setText(R.string.add_site)
toolbar.run {
setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { finish() }
inflateMenu(R.menu.menu_viewsite)
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.toHtml
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"

View file

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

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
@ -15,15 +14,7 @@
android:orientation="vertical"
>
<androidx.appcompat.widget.Toolbar
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"
/>
<include layout="@layout/include_app_bar"/>
<ScrollView
android:layout_width="match_parent"
@ -86,8 +77,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_text_spacing"
android:visibility="gone"
android:text="@string/warning_http_url"
android:visibility="gone"
style="@style/NockText.Footnote"
/>
@ -157,7 +148,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark"
android:background="@color/inkColorDark"
/>
<TextView

View file

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

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
@ -15,17 +14,7 @@
android:orientation="vertical"
>
<!-- Background is applied again here so programmatic elevation works -->
<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"
/>
<include layout="@layout/include_app_bar"/>
<ScrollView
android:id="@+id/scrollView"
@ -187,7 +176,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/colorPrimaryDark"
android:background="@color/inkColorDark"
/>
<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
android:id="@+id/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>

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>
</string-array>
<string-array name="donation_options">
<item>via PayPal</item>
<item>via Cash App</item>
<item>via Venmo</item>
</string-array>
</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"?>
<resources>
<color name="colorPrimary">#455A64</color>
<color name="colorPrimaryDark">#37474F</color>
<color name="colorPrimary_lightTheme">#FFFFFF</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="dividerColor">#EEEEEE</color>
</resources>

View file

@ -13,7 +13,8 @@
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
<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>.
]]></string>
]]></string>
<string name="dark_mode">Dark Mode</string>
<string name="dismiss">Dismiss</string>
<string name="add_site">Add Site</string>
@ -74,4 +75,15 @@
exception to pass custom error messages to Nock Nock.
</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>

View file

@ -1,27 +1,14 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<style name="AppTheme" parent="AppThemeParent"/>
<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="colorButtonNormal">@color/colorPrimaryDark</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="colorButtonNormal">@color/inkColor</item>
<item name="android:listDivider">@drawable/divider</item>
@ -37,28 +24,6 @@
<item name="android:windowBackground">@android:color/transparent</item>
</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">
<item name="android:textColor">#fff</item>
<item name="backgroundTint">@color/colorAccent</item>
@ -67,7 +32,7 @@
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
<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>
</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.mozilla:rhino:' + versions.rhino
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
testImplementation 'junit:junit:' + versions.junit
testImplementation 'com.google.truth:truth:' + versions.truth
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 = [
// Project
minSdk : 21,
compileSdk : 28,
buildTools : '28.0.3',
publishVersion : '0.8.0',
publishVersionCode : 28,
// Plugins
gradlePlugin : '3.2.1',
spotlessPlugin : '3.17.0',
versionPlugin : '0.20.0',
// Misc
okHttp : '3.12.1',
rhino : '1.7.10',
// Kotlin
kotlin : '1.3.11',
coroutines : '1.1.0',
koin : '1.0.2',
// Google/AndroidX
androidxAnnotations : '1.0.1',
androidxCore : '1.0.2',
androidxRecyclerView: '1.0.0',
androidxBrowser : '1.0.0',
googleMaterial : '1.0.0',
room : '2.0.0',
lifecycle : '2.0.0',
// Rx
rxBinding : '3.0.0-alpha1',
// afollestad
materialDialogs : '2.0.0-rc7',
rxkPrefs : '1.2.0',
rxkPrefs : '1.2.1',
// Debugging
timber : '4.7.1',
// Unit testing
junit : '4.12',
mockito : '2.23.4',
mockitoKotlin : '2.0.0-RC1',
truth : '0.42',
// UI testing
androidxTestRunner : '1.1.1',
androidxTest : '1.1.0',
archTesting : '2.0.0'