Dark mode

This commit is contained in:
Philipp Heckel 2022-01-19 21:05:41 -05:00
parent 71b8ea1e6d
commit 2d9171311f
14 changed files with 170 additions and 53 deletions

View file

@ -118,7 +118,7 @@
android:resource="@drawable/ic_notification"/>
<!-- FileProvider required for older Android versions (<= P), to allow passing the file URI in the open intent.
Avoids "exposed beyong app through Intent.getData" exception, see see https://stackoverflow.com/a/57288352/1440785 -->
Avoids "exposed beyond app through Intent.getData" exception, see see https://stackoverflow.com/a/57288352/1440785 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

@ -5,8 +5,10 @@ import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.*
import io.heckel.ntfy.log.Log
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
@ -179,6 +181,22 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
.apply()
}
fun setDarkMode(mode: Int) {
if (mode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
sharedPrefs.edit()
.remove(SHARED_PREFS_DARK_MODE)
.apply()
} else {
sharedPrefs.edit()
.putInt(SHARED_PREFS_DARK_MODE, mode)
.apply()
}
}
fun getDarkMode(): Int {
return sharedPrefs.getInt(SHARED_PREFS_DARK_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
fun getWakelockEnabled(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_WAKELOCK_ENABLED, false) // Disabled by default!
}
@ -358,12 +376,17 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
const val SHARED_PREFS_AUTO_DOWNLOAD_MAX_SIZE = "AutoDownload"
const val SHARED_PREFS_WAKELOCK_ENABLED = "WakelockEnabled"
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
const val SHARED_PREFS_DARK_MODE = "DarkMode"
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
const val MUTED_UNTIL_SHOW_ALL = 0L
const val MUTED_UNTIL_FOREVER = 1L
const val MUTED_UNTIL_TOMORROW = 2L
const val AUTO_DOWNLOAD_NEVER = 0L
const val AUTO_DOWNLOAD_ALWAYS = 1L
const val AUTO_DOWNLOAD_DEFAULT = 1024 * 1024L // Must match a value in values.xml

View file

@ -22,6 +22,7 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
@ -289,7 +290,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)
} else {
Log.d(TAG, "Re-enabling notifications ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
onNotificationMutedUntilChanged(0L)
onNotificationMutedUntilChanged(Repository.MUTED_UNTIL_SHOW_ALL)
}
}

View file

@ -16,6 +16,9 @@ import android.widget.Button
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
@ -150,6 +153,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
messenger.subscribe(ApiService.CONTROL_TOPIC)
// Darrkkkk mode
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
// Background things
startPeriodicPollWorker()
startPeriodicServiceRestartWorker()
@ -304,7 +310,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)
} else {
Log.d(TAG, "Re-enabling global notifications")
onNotificationMutedUntilChanged(0L)
onNotificationMutedUntilChanged(Repository.MUTED_UNTIL_SHOW_ALL)
}
}

View file

@ -64,6 +64,7 @@ class NotificationFragment : DialogFragment() {
muteUntilTomorrowButton = view.findViewById(R.id.notification_dialog_tomorrow)
muteUntilTomorrowButton.setOnClickListener {
// Duplicate code in SettingsActivity, :shrug: ...
val date = Calendar.getInstance()
date.add(Calendar.DAY_OF_MONTH, 1)
date.set(Calendar.HOUR_OF_DAY, 8)
@ -74,7 +75,7 @@ class NotificationFragment : DialogFragment() {
}
muteForeverButton = view.findViewById(R.id.notification_dialog_forever)
muteForeverButton.setOnClickListener{ onClick(1) }
muteForeverButton.setOnClickListener{ onClick(Repository.MUTED_UNTIL_FOREVER) }
return AlertDialog.Builder(activity)
.setView(view)

View file

@ -12,9 +12,9 @@ import android.text.TextUtils
import android.widget.Toast
import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener
@ -32,6 +32,7 @@ import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.*
import java.util.concurrent.TimeUnit
class SettingsActivity : AppCompatActivity() {
@ -44,7 +45,7 @@ class SettingsActivity : AppCompatActivity() {
Log.d(TAG, "Create $this")
if (savedInstanceState == null) {
fragment = SettingsFragment(supportFragmentManager)
fragment = SettingsFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_layout, fragment)
@ -58,7 +59,7 @@ class SettingsActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment(private val supportFragmentManager: FragmentManager) : PreferenceFragmentCompat() {
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var repository: Repository
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
@ -75,34 +76,43 @@ class SettingsActivity : AppCompatActivity() {
// Notifications muted until (global)
val mutedUntilPrefId = context?.getString(R.string.settings_notifications_muted_until_key) ?: return
val mutedUntilSummary = { s: Long ->
when (s) {
0L -> getString(R.string.settings_notifications_muted_until_enabled)
1L -> getString(R.string.settings_notifications_muted_until_disabled_forever)
else -> {
val formattedDate = formatDateShort(s)
getString(R.string.settings_notifications_muted_until_disabled_until, formattedDate)
}
}
}
val mutedUntil: Preference? = findPreference(mutedUntilPrefId)
mutedUntil?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
mutedUntil?.summary = mutedUntilSummary(repository.getGlobalMutedUntil())
mutedUntil?.onPreferenceClickListener = OnPreferenceClickListener {
if (repository.getGlobalMutedUntil() > 0) {
repository.setGlobalMutedUntil(0)
mutedUntil?.summary = mutedUntilSummary(0)
} else {
val notificationFragment = NotificationFragment()
notificationFragment.settingsListener = object : NotificationFragment.NotificationSettingsListener {
override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) {
val mutedUntil: ListPreference? = findPreference(mutedUntilPrefId)
mutedUntil?.value = repository.getGlobalMutedUntil().toString()
mutedUntil?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val mutedUntilValue = value?.toLongOrNull() ?:return
when (mutedUntilValue) {
Repository.MUTED_UNTIL_SHOW_ALL -> repository.setGlobalMutedUntil(mutedUntilValue)
Repository.MUTED_UNTIL_FOREVER -> repository.setGlobalMutedUntil(mutedUntilValue)
Repository.MUTED_UNTIL_TOMORROW -> {
val date = Calendar.getInstance()
date.add(Calendar.DAY_OF_MONTH, 1)
date.set(Calendar.HOUR_OF_DAY, 8)
date.set(Calendar.MINUTE, 30)
date.set(Calendar.SECOND, 0)
date.set(Calendar.MILLISECOND, 0)
repository.setGlobalMutedUntil(date.timeInMillis/1000)
}
else -> {
val mutedUntilTimestamp = System.currentTimeMillis()/1000 + mutedUntilValue * 60
repository.setGlobalMutedUntil(mutedUntilTimestamp)
mutedUntil?.summary = mutedUntilSummary(mutedUntilTimestamp)
}
}
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)
}
true
override fun getString(key: String?, defValue: String?): String {
return repository.getGlobalMutedUntil().toString()
}
}
mutedUntil?.summaryProvider = Preference.SummaryProvider<ListPreference> { _ ->
val mutedUntilValue = repository.getGlobalMutedUntil()
when (mutedUntilValue) {
Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all)
Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever)
else -> {
val formattedDate = formatDateShort(mutedUntilValue)
getString(R.string.settings_notifications_muted_until_x, formattedDate)
}
}
}
// Minimum priority
@ -163,6 +173,30 @@ class SettingsActivity : AppCompatActivity() {
}
}
// Dark mode
val darkModePrefId = context?.getString(R.string.settings_appearance_dark_mode_key) ?: return
val darkMode: ListPreference? = findPreference(darkModePrefId)
darkMode?.value = repository.getDarkMode().toString()
darkMode?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val darkModeValue = value?.toIntOrNull() ?: return
repository.setDarkMode(darkModeValue)
AppCompatDelegate.setDefaultNightMode(darkModeValue)
}
override fun getString(key: String?, defValue: String?): String {
return repository.getDarkMode().toString()
}
}
darkMode?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
val darkModeValue = pref.value.toIntOrNull() ?: repository.getDarkMode()
when (darkModeValue) {
AppCompatDelegate.MODE_NIGHT_NO -> getString(R.string.settings_appearance_dark_mode_summary_light)
AppCompatDelegate.MODE_NIGHT_YES -> getString(R.string.settings_appearance_dark_mode_summary_dark)
else -> getString(R.string.settings_appearance_dark_mode_summary_system)
}
}
// UnifiedPush enabled
val upEnabledPrefId = context?.getString(R.string.settings_unified_push_enabled_key) ?: return
val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId)

View file

@ -42,7 +42,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/detail_item_message_text"
android:textColor="@color/primaryTextColor"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:autoLink="web"
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
@ -54,7 +54,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/detail_item_title_text"
android:textColor="@color/primaryTextColor"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:autoLink="web"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
@ -112,7 +112,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/detail_item_attachment_info"
android:textColor="@color/primaryTextColor"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toEndOf="@+id/detail_item_attachment_icon"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:background="?android:attr/selectableItemBackground"
@ -22,7 +21,7 @@
app:layout_constraintBottom_toTopOf="@+id/main_item_status"
android:layout_marginStart="10dp" app:layout_constraintStart_toEndOf="@+id/main_item_image"
app:layout_constraintVertical_bias="0.0" android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/primaryTextColor" android:layout_marginTop="10dp"
android:textColor="?android:attr/textColorPrimary" android:layout_marginTop="10dp"
app:layout_constraintEnd_toStartOf="@+id/main_item_instant_image"/>
<TextView
android:text="89 notifications, reconnecting ... This may wrap in the case of UnifiedPush"

View file

@ -16,10 +16,10 @@
android:paddingBottom="8dp"
android:text="@string/notification_dialog_title"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" android:paddingStart="5dp" android:paddingEnd="5dp"
android:textColor="@color/primaryTextColor"/>
android:textColor="?android:attr/textColorPrimary"/>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -3,13 +3,9 @@
<color name="primaryColor">#338574</color>
<color name="primaryLightColor">#338574</color>
<color name="primaryDarkColor">#2A6E60</color>
<color name="primaryTextColor">#000000</color>
<color name="primaryLightTextColor">#FFFFFF</color>
<color name="primarySelectedRowColor">#EEEEEE</color>
<color name="primaryDangerButtonColor">#C30000</color>
<color name="primaryPriorityUrgentColor">#C30000</color>
<color name="primaryPriorityHighColor">#E10000</color>
</resources>

View file

@ -173,13 +173,14 @@
<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 now paused until %1$s</string>
<string name="notification_dialog_muted_until_toast_message">Notifications are 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>
<string name="notification_dialog_2h">2 hours</string>
<string name="notification_dialog_8h">8 hours</string>
<string name="notification_dialog_tomorrow">Until tomorrow</string>
<string name="notification_dialog_forever">Forever</string>
<string name="notification_dialog_forever">Until re-enabled</string>
<!-- Notification popup -->
<string name="notification_popup_action_open">Open</string>
@ -196,9 +197,9 @@
<string name="settings_notifications_header">Notifications</string>
<string name="settings_notifications_muted_until_key">MutedUntil</string>
<string name="settings_notifications_muted_until_title">Pause notifications</string>
<string name="settings_notifications_muted_until_enabled">All notifications will be displayed</string>
<string name="settings_notifications_muted_until_disabled_forever">Notifications muted until re-enabled</string>
<string name="settings_notifications_muted_until_disabled_until">Notifications muted until %1$s</string>
<string name="settings_notifications_muted_until_show_all">All notifications will be displayed</string>
<string name="settings_notifications_muted_until_forever">Notifications muted until re-enabled</string>
<string name="settings_notifications_muted_until_x">Notifications muted until %1$s</string>
<string name="settings_notifications_min_priority_key">MinPriority</string>
<string name="settings_notifications_min_priority_title">Minimum priority</string>
<string name="settings_notifications_min_priority_summary_any">Notifications of all priorities are shown</string>
@ -222,6 +223,15 @@
<string name="settings_notifications_auto_download_5m">If smaller than 5 MB</string>
<string name="settings_notifications_auto_download_10m">If smaller than 10 MB</string>
<string name="settings_notifications_auto_download_50m">If smaller than 50 MB</string>
<string name="settings_appearance_header">Appearance</string>
<string name="settings_appearance_dark_mode_key">DarkMode</string>
<string name="settings_appearance_dark_mode_title">Dark mode</string>
<string name="settings_appearance_dark_mode_summary_system">Use the system default</string>
<string name="settings_appearance_dark_mode_summary_light">Light mode is enabled</string>
<string name="settings_appearance_dark_mode_summary_dark">Dark mode is enabled. Are you a vampire?</string>
<string name="settings_appearance_dark_mode_entry_system">Use system default</string>
<string name="settings_appearance_dark_mode_entry_light">Light mode</string>
<string name="settings_appearance_dark_mode_entry_dark">Dark mode</string>
<string name="settings_unified_push_header">UnifiedPush</string>
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>

View file

@ -1,13 +1,19 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- DayNight mode for easy dark mode, see https://material.io/develop/android/theming/dark -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryVariant">@color/primaryDarkColor</item>
<item name="colorAccent">@color/primaryLightColor</item>
<item name="actionBarStyle">@style/Custom.ActionBar</item>
<item name="android:statusBarColor">@color/primaryColor</item>
<item name="actionModeBackground">@color/primaryDarkColor</item>
</style>
<!-- Rounded corners in images: https://stackoverflow.com/a/61960983/1440785 -->
<!-- Action bar color identical in dark mode, see https://stackoverflow.com/a/58368668/1440785 -->
<style name="Custom.ActionBar" parent="Widget.MaterialComponents.Light.ActionBar.Solid">
<item name="background">@color/primaryColor</item>
</style>
<!-- Rounded corners in images, see https://stackoverflow.com/a/61960983/1440785 -->
<style name="roundedCornersImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">5dp</item>

View file

@ -1,5 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="settings_notifications_muted_until_entries">
<item>@string/notification_dialog_show_all</item>
<item>@string/notification_dialog_30min</item>
<item>@string/notification_dialog_1h</item>
<item>@string/notification_dialog_2h</item>
<item>@string/notification_dialog_8h</item>
<item>@string/notification_dialog_tomorrow</item>
<item>@string/notification_dialog_forever</item>
</string-array>
<string-array name="settings_notifications_muted_until_values">
<item>0</item> <!-- See Repository -->
<item>30</item> <!-- See Minutes -->
<item>60</item>
<item>120</item>
<item>480</item>
<item>2</item> <!-- See Repository -->
<item>1</item> <!-- See Repository -->
</string-array>
<string-array name="settings_notifications_min_priority_entries">
<item>@string/settings_notifications_min_priority_min</item>
<item>@string/settings_notifications_min_priority_low</item>
@ -50,4 +68,15 @@
<item>copy</item>
<item>upload</item>
</string-array>
<string-array name="settings_appearance_dark_mode_entries">
<item>@string/settings_appearance_dark_mode_entry_system</item>
<item>@string/settings_appearance_dark_mode_entry_light</item>
<item>@string/settings_appearance_dark_mode_entry_dark</item>
</string-array>
<string-array name="settings_appearance_dark_mode_values">
<!-- Must match values in AppCompatDelegate -->
<item>-1</item>
<item>1</item>
<item>2</item>
</string-array>
</resources>

View file

@ -1,12 +1,14 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
app:title="@string/settings_title">
<PreferenceCategory
app:title="@string/settings_notifications_header"
app:layout="@layout/preference_category_material_edited">
<Preference
<ListPreference
app:key="@string/settings_notifications_muted_until_key"
app:title="@string/settings_notifications_muted_until_title"/>
app:title="@string/settings_notifications_muted_until_title"
app:entries="@array/settings_notifications_muted_until_entries"
app:entryValues="@array/settings_notifications_muted_until_values"
app:defaultValue="0"/>
<ListPreference
app:key="@string/settings_notifications_min_priority_key"
app:title="@string/settings_notifications_min_priority_title"
@ -20,6 +22,16 @@
app:entryValues="@array/settings_notifications_auto_download_values"
app:defaultValue="1"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/settings_appearance_header"
app:layout="@layout/preference_category_material_edited">
<ListPreference
app:key="@string/settings_appearance_dark_mode_key"
app:title="@string/settings_appearance_dark_mode_title"
app:entries="@array/settings_appearance_dark_mode_entries"
app:entryValues="@array/settings_appearance_dark_mode_values"
app:defaultValue="-1"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/settings_unified_push_header"
app:summary="@string/settings_unified_push_header_summary"