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">
<activity android:name="io.heckel.ntfy.ui.MainActivity"
android:icon="@drawable/ntfy"
android:label="@string/main_action_bar_label">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="io.heckel.ntfy.ui.DetailActivity" />
</application>
</manifest>

View file

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

View file

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

View file

@ -11,9 +11,9 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
class AddFragment(private val listener: Listener) : DialogFragment() {
interface Listener {
fun onAddClicked(topic: String, baseUrl: String)
class AddFragment(private val listener: AddSubscriptionListener) : DialogFragment() {
interface AddSubscriptionListener {
fun onAddSubscription(topic: String, baseUrl: String)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -34,7 +34,7 @@ class AddFragment(private val listener: Listener) : DialogFragment() {
} else {
getString(R.string.add_dialog_base_url_default)
}
listener.onAddClicked(topic, baseUrl)
listener.onAddSubscription(topic, baseUrl)
}
.setNegativeButton(R.string.add_dialog_button_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 kotlin.random.Random
const val SUBSCRIPTION_ID = "topic_id"
class MainActivity : AppCompatActivity(), AddFragment.Listener {
private val subscriptionViewModel by viewModels<SubscriptionsViewModel> {
class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
private val subscriptionsViewModel by viewModels<SubscriptionsViewModel> {
SubscriptionsViewModelFactory()
}
@ -33,30 +34,42 @@ class MainActivity : AppCompatActivity(), AddFragment.Listener {
super.onCreate(savedInstanceState)
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 ("+")
val fab: View = findViewById(R.id.fab)
fab.setOnClickListener {
fabOnClick()
onAddButtonClick()
}
// Update main list based on topicsViewModel (& its datasource/livedata)
val adapter = TopicsAdapter { topic -> subscriptionOnClick(topic) }
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.adapter = adapter
val noSubscriptionsText: View = findViewById(R.id.main_no_subscriptions_text)
val adapter = SubscriptionsAdapter(this) { subscription -> onUnsubscribe(subscription) }
val mainList: RecyclerView = findViewById(R.id.main_subscriptions_list)
mainList.adapter = adapter
subscriptionViewModel.list().observe(this) {
subscriptionsViewModel.list().observe(this) {
it?.let {
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
createNotificationChannel()
subscriptionViewModel.setListener { n -> displayNotification(n) }
subscriptionsViewModel.setListener { n -> displayNotification(n) }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu, menu)
menuInflater.inflate(R.menu.main_action_bar_menu, menu)
return true
}
@ -74,22 +87,18 @@ class MainActivity : AppCompatActivity(), AddFragment.Listener {
}
}
/* Opens detail view when list item is clicked. */
private fun subscriptionOnClick(subscription: Subscription) {
val intent = Intent(this, DetailActivity()::class.java)
intent.putExtra(SUBSCRIPTION_ID, subscription.id)
startActivity(intent)
private fun onUnsubscribe(subscription: Subscription) {
subscriptionsViewModel.remove(subscription)
}
/* Adds topic to topicList when FAB is clicked. */
private fun fabOnClick() {
private fun onAddButtonClick() {
val newFragment = AddFragment(this)
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)
subscriptionViewModel.add(subscription)
subscriptionsViewModel.add(subscription)
}
private fun displayNotification(n: Notification) {

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -11,54 +12,65 @@ import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Status
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) :
ListAdapter<Subscription, TopicsAdapter.TopicViewHolder>(TopicDiffCallback) {
class SubscriptionsAdapter(private val context: Context, private val onClick: (Subscription) -> Unit) :
ListAdapter<Subscription, SubscriptionsAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
/* 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) {
private var topic: Subscription? = null
private var subscription: Subscription? = null
private val context: Context = itemView.context
private val nameView: TextView = itemView.findViewById(R.id.topic_text)
private val statusView: TextView = itemView.findViewById(R.id.topic_status)
init {
itemView.setOnClickListener {
topic?.let {
onClick(it)
}
val popup = PopupMenu(context, itemView)
popup.inflate(R.menu.main_item_popup_menu)
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) {
this.topic = subscription
val statusText = when (subscription.status) {
Status.CONNECTING -> context.getString(R.string.status_connecting)
else -> context.getString(R.string.status_connected)
}
val statusMessage = if (subscription.messages == 1) {
context.getString(R.string.status_text_one, statusText, subscription.messages)
this.subscription = subscription
val notificationsCountMessage = if (subscription.messages == 1) {
context.getString(R.string.main_item_status_text_one, subscription.messages)
} 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)
statusView.text = statusMessage
val statusText = when (subscription.status) {
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. */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopicViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
val view = LayoutInflater.from(parent.context)
.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. */
override fun onBindViewHolder(holder: TopicViewHolder, position: Int) {
val topic = getItem(position)
holder.bind(topic)
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
val subscription = getItem(position)
holder.bind(subscription)
}
}

View file

@ -19,7 +19,8 @@
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/add_dialog_topic_text"
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
android:text="@string/add_dialog_use_another_server"
android:layout_width="match_parent"
@ -28,5 +29,5 @@
android:id="@+id/add_dialog_base_url_text"
android:layout_width="match_parent"
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>

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"?>
<!--
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"/>
android:id="@+id/main_subscriptions_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
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
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/fab_content_description"
android:contentDescription="@string/main_add_button_description"
android:src="@drawable/ic_add_black_24dp"
app:layout_constraintBottom_toBottomOf="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"
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="vertical">
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" android:clickable="true" android:focusable="true">
<TextView
android:text="ntfy.sh/example"
android:layout_width="match_parent"
@ -13,6 +14,6 @@
android:text="Subscribed, 0 notifications"
android:layout_width="match_parent"
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>

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>
<!-- Main app -->
<string name="app_name">Ntfy</string>
<!-- Notifications -->
@ -6,21 +7,20 @@
<string name="notification_channel_id">ntfy</string>
<!-- 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_url">https://heckel.io/ntfy-android</string>
<string name="main_menu_website_title">Visit ntfy.sh</string>
<string name="main_menu_website_url">https://ntfy.sh</string>
<!-- Main activity: List and such -->
<string name="status_connected">Connected</string>
<string name="status_connecting">Connecting</string>
<string name="status_text_one">%1$s, %2$d notification</string>
<string name="status_text_not_one">%1$s, %2$d notifications</string>
<string name="fab_content_description">fab</string>
<!-- Detail activity -->
<string name="remove_topic">Unsubscribe</string>
<string name="main_item_status_connecting">connecting …</string>
<string name="main_item_status_reconnecting">reconnecting …</string>
<string name="main_item_status_text_one">%1$d notification received</string>
<string name="main_item_status_text_not_one">%1$d notifications received</string>
<string name="main_item_popup_unsubscribe">Unsubscribe</string>
<string name="main_add_button_description">Add subscription</string>
<string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
<!-- Add dialog -->
<string name="add_dialog_title">Subscribe to topic</string>