diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 43c8c8581..fa12726c6 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -360,6 +360,22 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) return sharedPrefs.getString(SHARED_PREFS_CONNECTION_PROTOCOL, null) ?: CONNECTION_PROTOCOL_JSONHTTP } + fun setDateTimeFormat(dateTimeFormat: String) { + if (dateTimeFormat == DATETIME_FORMAT_LOCALE) { + sharedPrefs.edit { + remove(SHARED_PREFS_DATETIME_FORMAT) + } + } else { + sharedPrefs.edit { + putString(SHARED_PREFS_DATETIME_FORMAT, dateTimeFormat) + } + } + } + + fun getDateTimeFormat(): String { + return sharedPrefs.getString(SHARED_PREFS_DATETIME_FORMAT, DATETIME_FORMAT_LOCALE) ?: DATETIME_FORMAT_LOCALE + } + fun getBroadcastEnabled(): Boolean { return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default } @@ -611,6 +627,7 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) const val SHARED_PREFS_AUTO_DOWNLOAD_MAX_SIZE = "AutoDownload" const val SHARED_PREFS_AUTO_DELETE_SECONDS = "AutoDelete" const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol" + const val SHARED_PREFS_DATETIME_FORMAT = "DateTimeFormat" const val SHARED_PREFS_DARK_MODE = "DarkMode" const val SHARED_PREFS_DYNAMIC_COLORS = "DynamicColors" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" @@ -655,6 +672,9 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp" const val CONNECTION_PROTOCOL_WS = "ws" + const val DATETIME_FORMAT_LOCALE = "locale" + const val DATETIME_FORMAT_ISO8601 = "iso8601" + const val BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS = 1L const val BATTERY_OPTIMIZATIONS_REMIND_TIME_NEVER = Long.MAX_VALUE diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index b2554a875..6fa17dda5 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -783,7 +783,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet 0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show() 1L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show() else -> { - val formattedDate = formatDateShort(mutedUntilTimestamp) + val formattedDate = formatDateShort(this@DetailActivity, mutedUntilTimestamp) Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show() } } @@ -843,7 +843,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet notificationsDisabledForeverItem?.isVisible = subscriptionMutedUntil == 1L notificationsDisabledUntilItem?.isVisible = subscriptionMutedUntil > 1L if (subscriptionMutedUntil > 1L) { - val formattedDate = formatDateShort(subscriptionMutedUntil) + val formattedDate = formatDateShort(this, subscriptionMutedUntil) notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate) } } @@ -975,7 +975,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet val content = adapter.selected.joinToString("\n\n") { notificationId -> val notification = repository.getNotification(notificationId) notification?.let { - decodeMessage(it) + "\n" + Date(it.timestamp * 1000).toString() + decodeMessage(it) + "\n" + formatDateShort(this@DetailActivity, it.timestamp) }.orEmpty() } runOnUiThread { diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index dd2187f87..9c974d0aa 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -115,7 +115,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: val unmatchedTags = unmatchedTags(splitTags(notification.tags)) val message = maybeAppendActionErrors(formatMessage(notification), notification) - dateView.text = formatDateShort(notification.timestamp) + dateView.text = formatDateShort(context, notification.timestamp) if (notification.isMarkdown()) { messageView.autoLinkMask = 0 markwon.setMarkdown(messageView, message.toString()) @@ -353,7 +353,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: if (expired) { infos.add(context.getString(R.string.detail_item_download_info_not_downloaded_expired)) } else if (expires) { - infos.add(context.getString(R.string.detail_item_download_info_not_downloaded_expires_x, formatDateShort(attachment.expires))) + infos.add(context.getString(R.string.detail_item_download_info_not_downloaded_expires_x, formatDateShort(context, attachment.expires))) } else { infos.add(context.getString(R.string.detail_item_download_info_not_downloaded)) } @@ -363,7 +363,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: if (expired) { infos.add(context.getString(R.string.detail_item_download_info_deleted_expired)) } else if (expires) { - infos.add(context.getString(R.string.detail_item_download_info_deleted_expires_x, formatDateShort(attachment.expires))) + infos.add(context.getString(R.string.detail_item_download_info_deleted_expires_x, formatDateShort(context, attachment.expires))) } else { infos.add(context.getString(R.string.detail_item_download_info_deleted)) } @@ -371,7 +371,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: if (expired) { infos.add(context.getString(R.string.detail_item_download_info_download_failed_expired)) } else if (expires) { - infos.add(context.getString(R.string.detail_item_download_info_download_failed_expires_x, formatDateShort(attachment.expires))) + infos.add(context.getString(R.string.detail_item_download_info_download_failed_expires_x, formatDateShort(context, attachment.expires))) } else { infos.add(context.getString(R.string.detail_item_download_info_download_failed)) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt index f0293c79c..fa343eb69 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt @@ -253,7 +253,7 @@ class DetailSettingsActivity : AppCompatActivity() { 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) + val formattedDate = formatDateShort(requireContext(), mutedUntilValue) getString(R.string.settings_notifications_muted_until_x, formattedDate) } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 251a4ba0e..166114ac0 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -552,7 +552,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific notificationsDisabledForeverItem?.isVisible = mutedUntilSeconds == 1L notificationsDisabledUntilItem?.isVisible = mutedUntilSeconds > 1L if (mutedUntilSeconds > 1L) { - val formattedDate = formatDateShort(mutedUntilSeconds) + val formattedDate = formatDateShort(this@MainActivity, mutedUntilSeconds) notificationsDisabledUntilItem?.title = getString(R.string.main_menu_notifications_disabled_until, formattedDate) } } @@ -645,7 +645,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific 0L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show() 1L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show() else -> { - val formattedDate = formatDateShort(mutedUntilTimestamp) + val formattedDate = formatDateShort(this, mutedUntilTimestamp) Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show() } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt index d10a0ce67..780978a13 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -17,9 +17,9 @@ import io.heckel.ntfy.db.ConnectionState import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.util.displayName +import io.heckel.ntfy.util.formatDate +import io.heckel.ntfy.util.formatTime import io.heckel.ntfy.util.readBitmapFromUriOrNull -import java.text.DateFormat -import java.util.* class MainAdapter( private val repository: Repository, @@ -95,14 +95,13 @@ class MainAdapter( if (subscription.instant && subscription.connectionDetails.state == ConnectionState.CONNECTING) { statusMessage += ", " + context.getString(R.string.main_item_status_reconnecting) } - val date = Date(subscription.lastActive * 1000) - val dateStr = DateFormat.getDateInstance(DateFormat.SHORT).format(date) + val dateStr = formatDate(context, subscription.lastActive) val moreThanOneDay = System.currentTimeMillis()/1000 - subscription.lastActive > 24 * 60 * 60 - val sameDay = dateStr == DateFormat.getDateInstance(DateFormat.SHORT).format(Date()) // Omg this is horrible + val sameDay = dateStr == formatDate(context, System.currentTimeMillis() / 1000) val dateText = if (subscription.lastActive == 0L) { "" } else if (sameDay) { - DateFormat.getTimeInstance(DateFormat.SHORT).format(date) + formatTime(context, subscription.lastActive) } else if (!moreThanOneDay) { context.getString(R.string.main_item_date_yesterday) } else { diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 8ebf2f874..d5de8e322 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -209,7 +209,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere 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) + val formattedDate = formatDateShort(requireContext(), mutedUntilValue) getString(R.string.settings_notifications_muted_until_x, formattedDate) } } @@ -351,6 +351,26 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + val dateTimeFormatPrefId = context?.getString(R.string.settings_general_datetime_format_key) ?: return + val dateTimeFormat: ListPreference? = findPreference(dateTimeFormatPrefId) + dateTimeFormat?.value = repository.getDateTimeFormat() + dateTimeFormat?.preferenceDataStore = object : PreferenceDataStore() { + override fun putString(key: String?, value: String?) { + val dateTimeFormatValue = value ?: repository.getDateTimeFormat() + repository.setDateTimeFormat(dateTimeFormatValue) + } + + override fun getString(key: String?, defValue: String?): String { + return repository.getDateTimeFormat() + } + } + dateTimeFormat?.summaryProvider = Preference.SummaryProvider { pref -> + when (pref.value) { + Repository.DATETIME_FORMAT_ISO8601 -> getString(R.string.settings_general_datetime_format_summary_iso8601) + else -> getString(R.string.settings_general_datetime_format_summary_locale) + } + } + // Language val languagePrefId = context?.getString(R.string.settings_general_language_key) ?: return val language: ListPreference? = findPreference(languagePrefId) diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index e472fcefc..9e4630b68 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -47,8 +47,11 @@ import java.io.IOException import java.security.MessageDigest import java.security.SecureRandom import java.text.DateFormat +import java.text.SimpleDateFormat import java.text.StringCharacterIterator import java.util.Date +import java.util.Locale +import java.util.TimeZone import kotlin.math.abs import kotlin.math.absoluteValue import androidx.core.net.toUri @@ -130,8 +133,41 @@ fun normalizeBaseUrl(url: String): String { } fun formatDateShort(timestampSecs: Long): String { - val date = Date(timestampSecs*1000) - return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date) + return formatDateTime(Date(timestampSecs * 1000), Repository.DATETIME_FORMAT_LOCALE) +} + +fun formatDateShort(context: Context, timestampSecs: Long): String { + val repository = Repository.getInstance(context) + return formatDateTime(Date(timestampSecs * 1000), repository.getDateTimeFormat()) +} + +fun formatDate(context: Context, timestampSecs: Long): String { + val repository = Repository.getInstance(context) + val date = Date(timestampSecs * 1000) + return if (repository.getDateTimeFormat() == Repository.DATETIME_FORMAT_ISO8601) { + SimpleDateFormat("yyyy-MM-dd", Locale.US).format(date) + } else { + DateFormat.getDateInstance(DateFormat.SHORT).format(date) + } +} + +fun formatTime(context: Context, timestampSecs: Long): String { + val repository = Repository.getInstance(context) + val date = Date(timestampSecs * 1000) + return if (repository.getDateTimeFormat() == Repository.DATETIME_FORMAT_ISO8601) { + SimpleDateFormat("HH:mm", Locale.US).format(date) + } else { + DateFormat.getTimeInstance(DateFormat.SHORT).format(date) + } +} + +private fun formatDateTime(date: Date, dateTimeFormat: String): String { + return if (dateTimeFormat == Repository.DATETIME_FORMAT_ISO8601) { + SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) + .format(date) + } else { + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date) + } } fun toPriority(priority: Int?): Int { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ac24ce1c..74e74fda2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -387,6 +387,11 @@ Use system default Light mode Dark mode + Date/time format + Using locale default date/time format + Using ISO 8601 date/time format + Locale default + ISO 8601 Language Using system default System default diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 0b45baf59..bc5ecb775 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -22,6 +22,7 @@ DefaultBaseURL ManageUsers DarkMode + DateTimeFormat Language DynamicColors MessageBarEnabled @@ -208,4 +209,12 @@ 1 2 + + @string/settings_general_datetime_format_entry_locale + @string/settings_general_datetime_format_entry_iso8601 + + + locale + iso8601 + diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 1ef47d3ed..f8b451bf3 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -54,6 +54,12 @@ app:entries="@array/settings_general_dark_mode_entries" app:entryValues="@array/settings_general_dark_mode_values" app:defaultValue="-1"/> +