Remove detail view, replace with popup

This commit is contained in:
Philipp Heckel 2021-10-28 11:45:34 -04:00
parent 8727558069
commit 573ab5db19
14 changed files with 111 additions and 190 deletions

View file

@ -13,12 +13,11 @@
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name="io.heckel.ntfy.ui.MainActivity" <activity android:name="io.heckel.ntfy.ui.MainActivity"
android:icon="@drawable/ntfy" android:icon="@drawable/ntfy"
android:label="@string/main_action_bar_label"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="io.heckel.ntfy.ui.DetailActivity" />
</application> </application>
</manifest> </manifest>

View file

@ -61,7 +61,7 @@ class ConnectionManager(private val repository: Repository) {
} finally { } finally {
conn.disconnect() conn.disconnect()
} }
updateStatus(subscriptionId, Status.CONNECTING) updateStatus(subscriptionId, Status.RECONNECTING)
println("Connection terminated: $topicUrl") println("Connection terminated: $topicUrl")
} }

View file

@ -1,7 +1,7 @@
package io.heckel.ntfy.data package io.heckel.ntfy.data
enum class Status { enum class Status {
CONNECTED, CONNECTING CONNECTED, CONNECTING, RECONNECTING
} }
data class Subscription( data class Subscription(

View file

@ -11,9 +11,9 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R import io.heckel.ntfy.R
class AddFragment(private val listener: Listener) : DialogFragment() { class AddFragment(private val listener: AddSubscriptionListener) : DialogFragment() {
interface Listener { interface AddSubscriptionListener {
fun onAddClicked(topic: String, baseUrl: String) fun onAddSubscription(topic: String, baseUrl: String)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -34,7 +34,7 @@ class AddFragment(private val listener: Listener) : DialogFragment() {
} else { } else {
getString(R.string.add_dialog_base_url_default) getString(R.string.add_dialog_base_url_default)
} }
listener.onAddClicked(topic, baseUrl) listener.onAddSubscription(topic, baseUrl)
} }
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ -> .setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
dialog?.cancel() dialog?.cancel()

View file

@ -1,63 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 io.heckel.ntfy.ui
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import io.heckel.ntfy.R
import io.heckel.ntfy.data.topicShortUrl
class DetailActivity : AppCompatActivity() {
private val subscriptionsViewModel by viewModels<SubscriptionsViewModel> {
SubscriptionsViewModelFactory()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.detail_activity)
var subscriptionId: Long? = null
/* Connect variables to UI elements. */
val topicText: TextView = findViewById(R.id.topic_detail_url)
val removeButton: Button = findViewById(R.id.remove_button)
val bundle: Bundle? = intent.extras
if (bundle != null) {
subscriptionId = bundle.getLong(SUBSCRIPTION_ID)
}
// TODO This should probably fail hard if topicId is null
/* If currentTopicId is not null, get corresponding topic and set name, image and
description */
subscriptionId?.let {
val subscription = subscriptionsViewModel.get(it)
topicText.text = subscription?.let { s -> topicShortUrl(s) }
removeButton.setOnClickListener {
if (subscription != null) {
subscriptionsViewModel.remove(subscription)
}
finish()
}
}
}
}

View file

@ -22,10 +22,11 @@ import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.data.topicShortUrl import io.heckel.ntfy.data.topicShortUrl
import kotlin.random.Random import kotlin.random.Random
const val SUBSCRIPTION_ID = "topic_id" const val SUBSCRIPTION_ID = "topic_id"
class MainActivity : AppCompatActivity(), AddFragment.Listener { class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
private val subscriptionViewModel by viewModels<SubscriptionsViewModel> { private val subscriptionsViewModel by viewModels<SubscriptionsViewModel> {
SubscriptionsViewModelFactory() SubscriptionsViewModelFactory()
} }
@ -33,30 +34,42 @@ class MainActivity : AppCompatActivity(), AddFragment.Listener {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity) setContentView(R.layout.main_activity)
// Action bar
title = getString(R.string.main_action_bar_title)
supportActionBar?.setIcon(R.drawable.ntfy) // FIXME this doesn't work
// Floating action button ("+") // Floating action button ("+")
val fab: View = findViewById(R.id.fab) val fab: View = findViewById(R.id.fab)
fab.setOnClickListener { fab.setOnClickListener {
fabOnClick() onAddButtonClick()
} }
// Update main list based on topicsViewModel (& its datasource/livedata) // Update main list based on topicsViewModel (& its datasource/livedata)
val adapter = TopicsAdapter { topic -> subscriptionOnClick(topic) } val noSubscriptionsText: View = findViewById(R.id.main_no_subscriptions_text)
val recyclerView: RecyclerView = findViewById(R.id.recycler_view) val adapter = SubscriptionsAdapter(this) { subscription -> onUnsubscribe(subscription) }
recyclerView.adapter = adapter val mainList: RecyclerView = findViewById(R.id.main_subscriptions_list)
mainList.adapter = adapter
subscriptionViewModel.list().observe(this) { subscriptionsViewModel.list().observe(this) {
it?.let { it?.let {
adapter.submitList(it as MutableList<Subscription>) adapter.submitList(it as MutableList<Subscription>)
if (it.isEmpty()) {
mainList.visibility = View.GONE
noSubscriptionsText.visibility = View.VISIBLE
} else {
mainList.visibility = View.VISIBLE
noSubscriptionsText.visibility = View.GONE
}
} }
} }
// Set up notification channel // Set up notification channel
createNotificationChannel() createNotificationChannel()
subscriptionViewModel.setListener { n -> displayNotification(n) } subscriptionsViewModel.setListener { n -> displayNotification(n) }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu, menu) menuInflater.inflate(R.menu.main_action_bar_menu, menu)
return true return true
} }
@ -74,22 +87,18 @@ class MainActivity : AppCompatActivity(), AddFragment.Listener {
} }
} }
/* Opens detail view when list item is clicked. */ private fun onUnsubscribe(subscription: Subscription) {
private fun subscriptionOnClick(subscription: Subscription) { subscriptionsViewModel.remove(subscription)
val intent = Intent(this, DetailActivity()::class.java)
intent.putExtra(SUBSCRIPTION_ID, subscription.id)
startActivity(intent)
} }
/* Adds topic to topicList when FAB is clicked. */ private fun onAddButtonClick() {
private fun fabOnClick() {
val newFragment = AddFragment(this) val newFragment = AddFragment(this)
newFragment.show(supportFragmentManager, "AddFragment") newFragment.show(supportFragmentManager, "AddFragment")
} }
override fun onAddClicked(topic: String, baseUrl: String) { override fun onAddSubscription(topic: String, baseUrl: String) {
val subscription = Subscription(Random.nextLong(), topic, baseUrl, Status.CONNECTING, 0) val subscription = Subscription(Random.nextLong(), topic, baseUrl, Status.CONNECTING, 0)
subscriptionViewModel.add(subscription) subscriptionsViewModel.add(subscription)
} }
private fun displayNotification(n: Notification) { private fun displayNotification(n: Notification) {

View file

@ -4,6 +4,7 @@ import android.content.Context
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.PopupMenu
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
@ -11,54 +12,65 @@ import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.data.Status import io.heckel.ntfy.data.Status
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.data.topicUrl import io.heckel.ntfy.data.topicShortUrl
class TopicsAdapter(private val onClick: (Subscription) -> Unit) : class SubscriptionsAdapter(private val context: Context, private val onClick: (Subscription) -> Unit) :
ListAdapter<Subscription, TopicsAdapter.TopicViewHolder>(TopicDiffCallback) { ListAdapter<Subscription, SubscriptionsAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
class TopicViewHolder(itemView: View, val onClick: (Subscription) -> Unit) : class SubscriptionViewHolder(itemView: View, val onUnsubscribe: (Subscription) -> Unit) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
private var topic: Subscription? = null private var subscription: Subscription? = null
private val context: Context = itemView.context private val context: Context = itemView.context
private val nameView: TextView = itemView.findViewById(R.id.topic_text) private val nameView: TextView = itemView.findViewById(R.id.topic_text)
private val statusView: TextView = itemView.findViewById(R.id.topic_status) private val statusView: TextView = itemView.findViewById(R.id.topic_status)
init { init {
itemView.setOnClickListener { val popup = PopupMenu(context, itemView)
topic?.let { popup.inflate(R.menu.main_item_popup_menu)
onClick(it) popup.setOnMenuItemClickListener { item ->
} when (item.itemId) {
R.id.main_item_popup_unsubscribe -> {
subscription?.let { s -> onUnsubscribe(s) }
true
}
else -> false
}
}
itemView.setOnLongClickListener {
subscription?.let { popup.show() }
true
} }
} }
fun bind(subscription: Subscription) { fun bind(subscription: Subscription) {
this.topic = subscription this.subscription = subscription
val statusText = when (subscription.status) { val notificationsCountMessage = if (subscription.messages == 1) {
Status.CONNECTING -> context.getString(R.string.status_connecting) context.getString(R.string.main_item_status_text_one, subscription.messages)
else -> context.getString(R.string.status_connected)
}
val statusMessage = if (subscription.messages == 1) {
context.getString(R.string.status_text_one, statusText, subscription.messages)
} else { } else {
context.getString(R.string.status_text_not_one, statusText, subscription.messages) context.getString(R.string.main_item_status_text_not_one, subscription.messages)
} }
nameView.text = topicUrl(subscription) val statusText = when (subscription.status) {
statusView.text = statusMessage Status.CONNECTING -> notificationsCountMessage + ", " + context.getString(R.string.main_item_status_connecting)
Status.RECONNECTING -> notificationsCountMessage + ", " + context.getString(R.string.main_item_status_reconnecting)
else -> notificationsCountMessage
}
nameView.text = topicShortUrl(subscription)
statusView.text = statusText
} }
} }
/* Creates and inflates view and return TopicViewHolder. */ /* Creates and inflates view and return TopicViewHolder. */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopicViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.main_fragment_item, parent, false) .inflate(R.layout.main_fragment_item, parent, false)
return TopicViewHolder(view, onClick) return SubscriptionViewHolder(view, onClick)
} }
/* Gets current topic and uses it to bind view. */ /* Gets current topic and uses it to bind view. */
override fun onBindViewHolder(holder: TopicViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
val topic = getItem(position) val subscription = getItem(position)
holder.bind(topic) holder.bind(subscription)
} }
} }

View file

@ -19,7 +19,8 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/add_dialog_topic_text" android:id="@+id/add_dialog_topic_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"/> android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
android:maxLines="1" android:inputType="text"/>
<CheckBox <CheckBox
android:text="@string/add_dialog_use_another_server" android:text="@string/add_dialog_use_another_server"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -28,5 +29,5 @@
android:id="@+id/add_dialog_base_url_text" android:id="@+id/add_dialog_base_url_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:visibility="gone" android:layout_height="wrap_content" android:visibility="gone"
android:hint="@string/add_dialog_base_url_hint"/> android:hint="@string/add_dialog_base_url_hint" android:inputType="textUri" android:maxLines="1"/>
</LinearLayout> </LinearLayout>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:text="Delete topic"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
<TextView
android:id="@+id/topic_detail_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="https://ntfy.sh/..."/>
<Button
android:id="@+id/remove_button"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/remove_topic" />
</LinearLayout>

View file

@ -1,35 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/main_subscriptions_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"/> android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
<TextView
android:id="@+id/main_no_subscriptions_text"
android:text="@string/main_no_subscriptions_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:padding="50dp" app:layout_constraintBottom_toBottomOf="parent" android:gravity="center_horizontal"
android:textStyle="italic"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/fab_content_description" android:contentDescription="@string/main_add_button_description"
android:src="@drawable/ic_add_black_24dp" android:src="@drawable/ic_add_black_24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"
android:orientation="vertical"> android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" android:clickable="true" android:focusable="true">
<TextView <TextView
android:text="ntfy.sh/example" android:text="ntfy.sh/example"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,6 +14,6 @@
android:text="Subscribed, 0 notifications" android:text="Subscribed, 0 notifications"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/topic_status" android:layout_height="wrap_content" android:id="@+id/topic_status"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginLeft="16dp"/> android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginStart="16dp"/>
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,4 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/main_item_popup_unsubscribe"
android:title="@string/main_item_popup_unsubscribe"/>
</menu>

View file

@ -1,4 +1,5 @@
<resources> <resources>
<!-- Main app -->
<string name="app_name">Ntfy</string> <string name="app_name">Ntfy</string>
<!-- Notifications --> <!-- Notifications -->
@ -6,21 +7,20 @@
<string name="notification_channel_id">ntfy</string> <string name="notification_channel_id">ntfy</string>
<!-- Main activity: Action bar --> <!-- Main activity: Action bar -->
<string name="main_action_bar_label">Subscribed topics</string> <string name="main_action_bar_title">Subscribed topics</string>
<string name="main_menu_source_title">Show source &amp; license</string> <string name="main_menu_source_title">Show source &amp; license</string>
<string name="main_menu_source_url">https://heckel.io/ntfy-android</string> <string name="main_menu_source_url">https://heckel.io/ntfy-android</string>
<string name="main_menu_website_title">Visit ntfy.sh</string> <string name="main_menu_website_title">Visit ntfy.sh</string>
<string name="main_menu_website_url">https://ntfy.sh</string> <string name="main_menu_website_url">https://ntfy.sh</string>
<!-- Main activity: List and such --> <!-- Main activity: List and such -->
<string name="status_connected">Connected</string> <string name="main_item_status_connecting">connecting …</string>
<string name="status_connecting">Connecting</string> <string name="main_item_status_reconnecting">reconnecting …</string>
<string name="status_text_one">%1$s, %2$d notification</string> <string name="main_item_status_text_one">%1$d notification received</string>
<string name="status_text_not_one">%1$s, %2$d notifications</string> <string name="main_item_status_text_not_one">%1$d notifications received</string>
<string name="fab_content_description">fab</string> <string name="main_item_popup_unsubscribe">Unsubscribe</string>
<string name="main_add_button_description">Add subscription</string>
<!-- Detail activity --> <string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
<string name="remove_topic">Unsubscribe</string>
<!-- Add dialog --> <!-- Add dialog -->
<string name="add_dialog_title">Subscribe to topic</string> <string name="add_dialog_title">Subscribe to topic</string>