WIP subscription icon
This commit is contained in:
parent
2909d877f7
commit
a498d68bcf
6 changed files with 98 additions and 23 deletions
|
@ -81,7 +81,7 @@ dependencies {
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
||||||
|
|
||||||
// Firebase, sigh ... (only Google Play)
|
// Firebase, sigh ... (only Google Play)
|
||||||
playImplementation 'com.google.firebase:firebase-messaging:23.0.3'
|
playImplementation 'com.google.firebase:firebase-messaging:23.0.4'
|
||||||
|
|
||||||
// RecyclerView
|
// RecyclerView
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.0-alpha02"
|
implementation "androidx.recyclerview:recyclerview:1.3.0-alpha02"
|
||||||
|
@ -90,7 +90,7 @@ dependencies {
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
|
||||||
// Material design
|
// Material design
|
||||||
implementation "com.google.android.material:material:1.5.0"
|
implementation "com.google.android.material:material:1.6.0"
|
||||||
|
|
||||||
// LiveData
|
// LiveData
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Notification
|
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.msg.DownloadWorker
|
import io.heckel.ntfy.msg.DownloadWorker
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import okio.source
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -70,7 +70,10 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
private lateinit var repository: Repository
|
private lateinit var repository: Repository
|
||||||
private lateinit var serviceManager: SubscriberServiceManager
|
private lateinit var serviceManager: SubscriberServiceManager
|
||||||
private lateinit var subscription: Subscription
|
private lateinit var subscription: Subscription
|
||||||
private lateinit var pickIconLauncher: ActivityResultLauncher<String>
|
|
||||||
|
private lateinit var iconSetPref: Preference
|
||||||
|
private lateinit var iconSetLauncher: ActivityResultLauncher<String>
|
||||||
|
private lateinit var iconRemovePref: Preference
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.detail_preferences, rootKey)
|
setPreferencesFromResource(R.xml.detail_preferences, rootKey)
|
||||||
|
@ -80,7 +83,7 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
serviceManager = SubscriberServiceManager(requireActivity())
|
serviceManager = SubscriberServiceManager(requireActivity())
|
||||||
|
|
||||||
// Create result launcher for custom icon (must be created in onCreatePreferences() directly)
|
// Create result launcher for custom icon (must be created in onCreatePreferences() directly)
|
||||||
pickIconLauncher = createCustomIconPickLauncher()
|
iconSetLauncher = createIconPickLauncher()
|
||||||
|
|
||||||
// Load subscription and users
|
// Load subscription and users
|
||||||
val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
|
val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
|
||||||
|
@ -99,7 +102,8 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
loadMutedUntilPref()
|
loadMutedUntilPref()
|
||||||
loadMinPriorityPref()
|
loadMinPriorityPref()
|
||||||
loadAutoDeletePref()
|
loadAutoDeletePref()
|
||||||
loadCustomIconsPref()
|
loadIconSetPref()
|
||||||
|
loadIconRemovePref()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInstantPref() {
|
private fun loadInstantPref() {
|
||||||
|
@ -233,41 +237,78 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCustomIconsPref() {
|
private fun loadIconSetPref() {
|
||||||
val prefId = context?.getString(R.string.detail_settings_general_icon_key) ?: return
|
val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return
|
||||||
val pref: Preference? = findPreference(prefId)
|
iconSetPref = findPreference(prefId) ?: return
|
||||||
pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
|
iconSetPref.isVisible = subscription.icon == null
|
||||||
pref?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
pref?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||||
pickIconLauncher.launch("image/*")
|
iconSetLauncher.launch("image/*")
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCustomIconPickLauncher(): ActivityResultLauncher<String> {
|
private fun loadIconRemovePref() {
|
||||||
|
val prefId = context?.getString(R.string.detail_settings_appearance_icon_remove_key) ?: return
|
||||||
|
iconRemovePref = findPreference(prefId) ?: return
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
|
||||||
|
if (subscription.icon != null) {
|
||||||
|
try {
|
||||||
|
val resolver = requireContext().applicationContext.contentResolver
|
||||||
|
val bitmapStream = resolver.openInputStream(Uri.parse(subscription.icon))
|
||||||
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
|
iconRemovePref.icon = bitmap.toDrawable(resources)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// FIXME
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconRemovePref.isVisible = subscription.icon != null
|
||||||
|
iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
|
iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||||
|
save(subscription.copy(icon = null))
|
||||||
|
iconRemovePref.isVisible = false
|
||||||
|
iconSetPref.isVisible = true
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIconPickLauncher(): ActivityResultLauncher<String> {
|
||||||
return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
|
return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
|
||||||
if (inputUri == null) {
|
if (inputUri == null) {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
// Write to cache storage
|
||||||
val resolver = requireContext().applicationContext.contentResolver
|
val resolver = requireContext().applicationContext.contentResolver
|
||||||
val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading")
|
val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading")
|
||||||
val outputUri = createUri()
|
val outputUri = createUri()
|
||||||
val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing")
|
val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing")
|
||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
save(subscription.copy(icon = outputUri.toString()))
|
save(subscription.copy(icon = outputUri.toString()))
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
// FIXME
|
||||||
|
|
||||||
|
iconSetPref.isVisible = false
|
||||||
|
|
||||||
|
val bitmapStream = resolver.openInputStream(Uri.parse(outputUri.toString()))
|
||||||
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
|
iconRemovePref.icon = bitmap.toDrawable(resources)
|
||||||
|
iconRemovePref.isVisible = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Saving icon failed", e)
|
Log.w(TAG, "Saving icon failed", e)
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
// FIXME
|
// FIXME TOAST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createUri(): Uri {
|
private fun createUri(): Uri {
|
||||||
val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
|
val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
|
||||||
if (!dir.exists() && !dir.mkdirs()) {
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
@ -277,6 +318,10 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
return FileProvider.getUriForFile(requireContext(), DownloadWorker.FILE_PROVIDER_AUTHORITY, file)
|
return FileProvider.getUriForFile(requireContext(), DownloadWorker.FILE_PROVIDER_AUTHORITY, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadBitmap() {
|
||||||
|
// FIXME
|
||||||
|
}
|
||||||
|
|
||||||
private fun save(newSubscription: Subscription, refresh: Boolean = false) {
|
private fun save(newSubscription: Subscription, refresh: Boolean = false) {
|
||||||
subscription = newSubscription
|
subscription = newSubscription
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
@ -13,6 +16,8 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.ConnectionState
|
import io.heckel.ntfy.db.ConnectionState
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -47,6 +52,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
||||||
RecyclerView.ViewHolder(itemView) {
|
RecyclerView.ViewHolder(itemView) {
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
private val context: Context = itemView.context
|
private val context: Context = itemView.context
|
||||||
|
private val imageView: ImageView = itemView.findViewById(R.id.main_item_image)
|
||||||
private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
|
private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
|
||||||
private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
|
private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
|
||||||
private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
|
private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
|
||||||
|
@ -84,6 +90,16 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
||||||
val globalMutedUntil = repository.getGlobalMutedUntil()
|
val globalMutedUntil = repository.getGlobalMutedUntil()
|
||||||
val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && !isUnifiedPush
|
val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && !isUnifiedPush
|
||||||
val showMutedUntilIcon = !showMutedForeverIcon && (subscription.mutedUntil > 1L || globalMutedUntil > 1L) && !isUnifiedPush
|
val showMutedUntilIcon = !showMutedForeverIcon && (subscription.mutedUntil > 1L || globalMutedUntil > 1L) && !isUnifiedPush
|
||||||
|
if (subscription.icon != null) {
|
||||||
|
try {
|
||||||
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
val bitmapStream = resolver.openInputStream(Uri.parse(subscription.icon))
|
||||||
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
|
imageView.setImageBitmap(bitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Cannot load subscription icon", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
|
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||||
statusView.text = statusMessage
|
statusView.text = statusMessage
|
||||||
dateView.text = dateText
|
dateView.text = dateText
|
||||||
|
@ -114,4 +130,8 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "NtfyMainAdapter"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -339,7 +339,10 @@
|
||||||
<string name="detail_settings_notifications_instant_title">Instant delivery</string>
|
<string name="detail_settings_notifications_instant_title">Instant delivery</string>
|
||||||
<string name="detail_settings_notifications_instant_summary_on">Notifications are delivered instantly. Requires a foreground service and consumes more battery.</string>
|
<string name="detail_settings_notifications_instant_summary_on">Notifications are delivered instantly. Requires a foreground service and consumes more battery.</string>
|
||||||
<string name="detail_settings_notifications_instant_summary_off">Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery.</string>
|
<string name="detail_settings_notifications_instant_summary_off">Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery.</string>
|
||||||
<string name="detail_settings_general_icon_title">Custom icon</string>
|
<string name="detail_settings_appearance_header">Appearance</string>
|
||||||
|
<string name="detail_settings_appearance_icon_title">Subscription icon</string>
|
||||||
|
<string name="detail_settings_appearance_icon_set_summary_set">This icon is displayed in notifications. Tap to remove it.</string>
|
||||||
|
<string name="detail_settings_appearance_icon_set_summary_no_set">Set an icon to be displayed in notifications</string>
|
||||||
<string name="detail_settings_global_setting_title">Use global setting</string>
|
<string name="detail_settings_global_setting_title">Use global setting</string>
|
||||||
<string name="detail_settings_global_setting_suffix">global</string>
|
<string name="detail_settings_global_setting_suffix">global</string>
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
<string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string>
|
<string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string>
|
||||||
<string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
|
<string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
|
||||||
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
|
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
|
||||||
<string name="detail_settings_general_icon_key" translatable="false">SubscriptionIcon</string>
|
<string name="detail_settings_appearance_icon_set_key" translatable="false">SubscriptionIconSet</string>
|
||||||
|
<string name="detail_settings_appearance_icon_remove_key" translatable="false">SubscriptionIconRemove</string>
|
||||||
|
|
||||||
<!-- Main settings -->
|
<!-- Main settings -->
|
||||||
<string-array name="settings_notifications_muted_until_entries">
|
<string-array name="settings_notifications_muted_until_entries">
|
||||||
|
|
|
@ -27,10 +27,16 @@
|
||||||
app:defaultValue="-1"
|
app:defaultValue="-1"
|
||||||
app:isPreferenceVisible="false"/> <!-- Same as Repository.AUTO_DELETE_USE_GLOBAL -->
|
app:isPreferenceVisible="false"/> <!-- Same as Repository.AUTO_DELETE_USE_GLOBAL -->
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_general_header">
|
<PreferenceCategory app:title="@string/detail_settings_appearance_header">
|
||||||
<Preference
|
<Preference
|
||||||
app:key="@string/detail_settings_general_icon_key"
|
app:key="@string/detail_settings_appearance_icon_set_key"
|
||||||
app:title="@string/detail_settings_general_icon_title"
|
app:title="@string/detail_settings_appearance_icon_title"
|
||||||
|
app:summary="@string/detail_settings_appearance_icon_set_summary_no_set"
|
||||||
|
app:isPreferenceVisible="false"/>
|
||||||
|
<Preference
|
||||||
|
app:key="@string/detail_settings_appearance_icon_remove_key"
|
||||||
|
app:title="@string/detail_settings_appearance_icon_title"
|
||||||
|
app:summary="@string/detail_settings_appearance_icon_set_summary_set"
|
||||||
app:isPreferenceVisible="false"/>
|
app:isPreferenceVisible="false"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
Loading…
Reference in a new issue