Styles for dark mode to match guidelines, closes #119

This commit is contained in:
Philipp Heckel 2022-02-07 18:35:36 -05:00
parent 678be49bff
commit e6c3a2e2bd
15 changed files with 140 additions and 52 deletions

View file

@ -12,6 +12,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.db.*
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.ui.Colors
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
@ -52,7 +53,7 @@ class NotificationService(val context: Context) {
val channelId = toChannelId(notification.priority)
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.primaryColor))
.setColor(ContextCompat.getColor(context, Colors.notificationIcon))
.setContentTitle(title)
.setOnlyAlertOnce(true) // Do not vibrate or play sound if already showing (updates!)
.setAutoCancel(true) // Cancel when notification is clicked

View file

@ -19,6 +19,7 @@ import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.ui.Colors
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.CoroutineScope
@ -292,7 +293,7 @@ class SubscriberService : Service() {
}
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_instant)
.setColor(ContextCompat.getColor(this, R.color.primaryColor))
.setColor(ContextCompat.getColor(this, Colors.notificationIcon))
.setContentTitle(title)
.setContentText(text)
.setContentIntent(pendingIntent)

View file

@ -0,0 +1,28 @@
package io.heckel.ntfy.ui
import android.content.Context
import io.heckel.ntfy.R
import io.heckel.ntfy.util.isDarkThemeOn
class Colors {
companion object {
const val refreshProgressIndicator = R.color.teal
const val notificationIcon = R.color.teal
fun itemSelectedBackground(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.gray_dark else R.color.gray_light
}
fun statusBarNormal(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_light else R.color.teal
}
fun statusBarActionMode(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_light else R.color.teal_dark
}
fun dangerText(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.red_light else R.color.red_dark
}
}
}

View file

@ -106,7 +106,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
// Swipe to refresh
mainListContainer = findViewById(R.id.detail_notification_list_container)
mainListContainer.setOnRefreshListener { refresh() }
mainListContainer.setColorSchemeResources(R.color.primaryColor)
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator)
// Update main list based on viewModel (& its datasource/livedata)
val noEntriesText: View = findViewById(R.id.detail_no_notifications)
@ -477,7 +477,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextColor(ContextCompat.getColor(this, R.color.primaryDangerButtonColor))
.setTextAppearance(R.style.DangerText)
}
dialog.show()
}
@ -512,7 +512,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextColor(ContextCompat.getColor(this, R.color.primaryDangerButtonColor))
.setTextAppearance(R.style.DangerText)
}
dialog.show()
}
@ -619,7 +619,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextColor(ContextCompat.getColor(this, R.color.primaryDangerButtonColor))
.setTextAppearance(R.style.DangerText)
}
dialog.show()
}
@ -634,8 +634,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
redrawList()
// Fade status bar color
val fromColor = ContextCompat.getColor(this, R.color.primaryColor)
val toColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
fadeStatusBarColor(window, fromColor, toColor)
}
@ -650,8 +650,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
redrawList()
// Fade status bar color
val fromColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
val toColor = ContextCompat.getColor(this, R.color.primaryColor)
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
fadeStatusBarColor(window, fromColor, toColor)
}

View file

@ -96,8 +96,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
tagsView.visibility = View.GONE
}
if (selected.contains(notification.id)) {
val backgroundColor = if (isDarkThemeOn(context, repository)) R.color.primaryDarkSelectedRowColor else R.color.primaryLightSelectedRowColor
itemView.setBackgroundResource(backgroundColor);
itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
}
renderPriority(context, notification)
maybeRenderAttachment(context, notification)

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.work.*
import com.google.android.material.floatingactionbutton.FloatingActionButton
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
@ -56,7 +57,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private lateinit var mainList: RecyclerView
private lateinit var mainListContainer: SwipeRefreshLayout
private lateinit var adapter: MainAdapter
private lateinit var fab: View
private lateinit var fab: FloatingActionButton
// Other stuff
private var actionMode: ActionMode? = null
@ -88,7 +89,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Swipe to refresh
mainListContainer = findViewById(R.id.main_subscriptions_list_container)
mainListContainer.setOnRefreshListener { refreshAllSubscriptions() }
mainListContainer.setColorSchemeResources(R.color.primaryColor)
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator)
// Update main list based on viewModel (& its datasource/livedata)
val noEntries: View = findViewById(R.id.main_no_subscriptions)
@ -512,7 +513,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextColor(ContextCompat.getColor(this, R.color.primaryDangerButtonColor))
.setTextAppearance(R.style.DangerText)
}
dialog.show()
}
@ -539,8 +540,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
})
// Fade status bar color
val fromColor = ContextCompat.getColor(this, R.color.primaryColor)
val toColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
fadeStatusBarColor(window, fromColor, toColor)
}
@ -568,8 +569,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
})
// Fade status bar color
val fromColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
val toColor = ContextCompat.getColor(this, R.color.primaryColor)
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
fadeStatusBarColor(window, fromColor, toColor)
}

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@ -102,8 +101,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
itemView.setOnClickListener { onClick(subscription) }
itemView.setOnLongClickListener { onLongClick(subscription); true }
if (selected.contains(subscription.id)) {
val backgroundColor = if (isDarkThemeOn(context, repository)) R.color.primaryDarkSelectedRowColor else R.color.primaryLightSelectedRowColor
itemView.setBackgroundResource(backgroundColor)
itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
}
}
}

View file

@ -3,6 +3,7 @@ package io.heckel.ntfy.ui
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -15,7 +16,6 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
import io.heckel.ntfy.db.User
import kotlin.random.Random
class UserFragment : DialogFragment() {
private var user: User? = null
@ -97,9 +97,15 @@ class UserFragment : DialogFragment() {
// Delete button should be red
if (user != null) {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextColor(ContextCompat.getColor(requireContext(), R.color.primaryDangerButtonColor))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextAppearance(R.style.DangerText)
} else {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextColor(ContextCompat.getColor(requireContext(), Colors.dangerText(requireContext())))
}
}
// Validate input when typing

View file

@ -3,17 +3,14 @@ package io.heckel.ntfy.util
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.Window
import androidx.appcompat.app.AppCompatDelegate
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
@ -206,17 +203,17 @@ fun isIgnoringBatteryOptimizations(context: Context): Boolean {
}
// Returns true if dark mode is on, see https://stackoverflow.com/a/60761189/1440785
fun Context.isDarkThemeOn(): Boolean {
fun Context.systemDarkThemeOn(): Boolean {
return resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
fun isDarkThemeOn(context: Context, repository: Repository): Boolean {
val darkMode = repository.getDarkMode()
fun isDarkThemeOn(context: Context): Boolean {
val darkMode = Repository.getInstance(context).getDarkMode()
if (darkMode == AppCompatDelegate.MODE_NIGHT_YES) {
return true
}
if (darkMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && context.isDarkThemeOn()) {
if (darkMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && context.systemDarkThemeOn()) {
return true
}
return false

View file

@ -139,11 +139,12 @@
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_margin="24dp"
android:contentDescription="@string/main_add_button_description"
android:src="@drawable/ic_add_black_24dp"
app:tint="@color/primaryLightTextColor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:backgroundTint="@color/primaryColor"/>
app:layout_constraintEnd_toEndOf="parent"
style="@style/FloatingActionButton"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -139,7 +139,12 @@
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
android:paddingStart="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description" android:paddingEnd="4dp" android:textColor="@color/primaryDangerButtonColor" app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image" android:layout_marginTop="5dp" tools:visibility="gone"/>
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description"
android:paddingEnd="4dp"
android:textAppearance="@style/DangerText"
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
android:layout_marginTop="5dp"
tools:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<ScrollView
@ -197,7 +202,10 @@
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error_text"
android:paddingStart="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_password" android:paddingEnd="4dp" android:textColor="@color/primaryDangerButtonColor" app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_password"
android:paddingEnd="4dp"
android:textAppearance="@style/DangerText"
app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="25dp"

View file

@ -0,0 +1,33 @@
<resources>
<!--
This file contains only overrides for the dark theme.
Also see "ui/Colors.kt" for colors that have to be defined in code.
Resources:
- https://material.io/design/color/dark-theme.html
- https://material.io/develop/android/theming/dark
- https://developer.android.com/codelabs/basic-android-kotlin-training-change-app-theme#4
- https://developer.android.com/guide/topics/ui/look-and-feel/themes
-->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/teal_light</item>
<item name="colorAccent">@color/teal_light</item> <!-- checkboxes, text fields -->
<item name="android:colorBackground">@color/black_light</item> <!-- background -->
<item name="android:statusBarColor">@color/black_light</item>
<item name="actionModeBackground">@color/black_light</item>
<!-- Action bar background & text color -->
<item name="colorSurface">@color/gray_dark</item>
<item name="colorOnSurface">@color/white</item>
</style>
<style name="DangerText" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/red_light</item>
</style>
<style name="FloatingActionButton" parent="@style/Widget.MaterialComponents.FloatingActionButton">
<item name="tint">@color/black_light</item>
<item name="backgroundTint">@color/teal_light</item>
</style>
</resources>

View file

@ -1,13 +1,16 @@
<!--?xml version="1.0" encoding="UTF-8"?-->
<resources>
<color name="primaryColor">#338574</color>
<color name="primaryDangerButtonColor">#C30000</color>
<color name="black">#ff000000</color>
<color name="black_light">#121212</color> <!-- Main dark mode surface color, as per style guide -->
<color name="white">#ffffffff</color>
<color name="primaryLightColor">#338574</color>
<color name="primaryLightTextColor">#FFFFFF</color>
<color name="primaryLightSelectedRowColor">#EEEEEE</color>
<color name="teal">#338574</color> <!-- Primary color (light mode) -->
<color name="teal_light">#65b5a3</color> <!-- Primary color (dark mode) -->
<color name="teal_dark">#2a6e60</color> <!-- Action bar background in action mode (light mode) -->
<color name="red_light">#fe4d2e</color> <!-- Danger text (dark mode) -->
<color name="red_dark">#c30000</color> <!-- Danger text (light mode) -->
<color name="primaryDarkColor">#2A6E60</color>
<color name="primaryDarkSelectedRowColor">#444444</color>
<color name="gray_light">#eeeeee</color> <!-- Light theme item selection -->
<color name="gray_dark">#282828</color> <!-- Dark mode action bar & item selection -->
</resources>

View file

@ -188,8 +188,8 @@
<string name="notification_dialog_cancel">Cancel</string>
<string name="notification_dialog_save">Save</string>
<string name="notification_dialog_enabled_toast_message">Notifications re-enabled</string>
<string name="notification_dialog_muted_forever_toast_message">Notifications are now paused</string>
<string name="notification_dialog_muted_until_toast_message">Notifications are paused until %1$s</string>
<string name="notification_dialog_muted_forever_toast_message">Notifications paused</string>
<string name="notification_dialog_muted_until_toast_message">Notifications paused until %1$s</string>
<string name="notification_dialog_show_all">Show all notifications</string>
<string name="notification_dialog_30min">30 minutes</string>
<string name="notification_dialog_1h">1 hour</string>

View file

@ -1,10 +1,22 @@
<resources>
<!-- DayNight mode for easy dark mode, see https://material.io/develop/android/theming/dark -->
<!-- Main app theme; dark theme styles see values-night/styles.xml -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorAccent">@color/primaryLightColor</item>
<item name="android:statusBarColor">@color/primaryColor</item>
<item name="actionModeBackground">@color/primaryDarkColor</item>
<item name="colorPrimary">@color/teal</item>
<item name="colorAccent">@color/teal</item> <!-- checkboxes, text fields -->
<item name="android:colorBackground">@color/white</item> <!-- background -->
<item name="android:statusBarColor">@color/teal</item>
<item name="actionModeBackground">@color/teal_dark</item>
</style>
<!-- Danger buttons & text -->
<style name="DangerText" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/red_dark</item>
</style>
<!-- Floating action button -->
<style name="FloatingActionButton" parent="@style/Widget.MaterialComponents.FloatingActionButton">
<item name="tint">@color/white</item>
<item name="backgroundTint">@color/teal</item>
</style>
<!-- Rounded corners in images, see https://stackoverflow.com/a/61960983/1440785 -->