We're getting there
This commit is contained in:
parent
638c8f093a
commit
49b3898977
10 changed files with 91 additions and 144 deletions
|
@ -1,19 +1,3 @@
|
||||||
/*
|
|
||||||
* 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
|
package io.heckel.ntfy
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
@ -32,11 +16,11 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.heckel.ntfy.add.AddTopicActivity
|
import io.heckel.ntfy.add.AddTopicActivity
|
||||||
import io.heckel.ntfy.data.Topic
|
import io.heckel.ntfy.data.Topic
|
||||||
import io.heckel.ntfy.detail.DetailActivity
|
import io.heckel.ntfy.detail.DetailActivity
|
||||||
import io.heckel.ntfy.list.*
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
const val TOPIC_ID = "topic id"
|
const val TOPIC_ID = "topic_id"
|
||||||
const val TOPIC_URL = "url"
|
const val TOPIC_NAME = "topic_name"
|
||||||
|
const val TOPIC_BASE_URL = "base_url"
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val newTopicActivityRequestCode = 1
|
private val newTopicActivityRequestCode = 1
|
||||||
|
@ -88,9 +72,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) {
|
if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) {
|
||||||
intentData?.let { data ->
|
intentData?.let { data ->
|
||||||
val topicId = Random.nextLong()
|
val name = data.getStringExtra(TOPIC_NAME) ?: return
|
||||||
val topicUrl = data.getStringExtra(TOPIC_URL) ?: return
|
val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return
|
||||||
val topic = Topic(topicId, topicUrl)
|
val topic = Topic(Random.nextLong(), name, baseUrl)
|
||||||
|
|
||||||
topicsViewModel.add(topic)
|
topicsViewModel.add(topic)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,4 @@
|
||||||
/*
|
package io.heckel.ntfy
|
||||||
* 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.list
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -23,7 +7,6 @@ import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.heckel.ntfy.R
|
|
||||||
import io.heckel.ntfy.data.Topic
|
import io.heckel.ntfy.data.Topic
|
||||||
|
|
||||||
class TopicsAdapter(private val onClick: (Topic) -> Unit) :
|
class TopicsAdapter(private val onClick: (Topic) -> Unit) :
|
||||||
|
@ -43,10 +26,11 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bind topic name and image. */
|
|
||||||
fun bind(topic: Topic) {
|
fun bind(topic: Topic) {
|
||||||
currentTopic = topic
|
currentTopic = topic
|
||||||
topicTextView.text = topic.url
|
val shortBaseUrl = topic.baseUrl.replace("https://", "") // Leave http:// untouched
|
||||||
|
val shortName = itemView.context.getString(R.string.topic_short_name_format, shortBaseUrl, topic.name)
|
||||||
|
topicTextView.text = shortName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +55,6 @@ object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean {
|
override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.name == newItem.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,14 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.heckel.ntfy.data.TopicsRepository
|
import io.heckel.ntfy.data.Repository
|
||||||
import io.heckel.ntfy.data.Topic
|
import io.heckel.ntfy.data.Topic
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlin.collections.List
|
import kotlin.collections.List
|
||||||
|
|
||||||
data class Notification(val topic: String, val message: String)
|
data class Notification(val topic: String, val message: String)
|
||||||
typealias NotificationListener = (notification: Notification) -> Unit
|
typealias NotificationListener = (notification: Notification) -> Unit
|
||||||
|
|
||||||
class TopicsViewModel(private val repository: TopicsRepository) : ViewModel() {
|
class TopicsViewModel(private val repository: Repository) : ViewModel() {
|
||||||
fun add(topic: Topic) {
|
fun add(topic: Topic) {
|
||||||
repository.add(topic, viewModelScope)
|
repository.add(topic, viewModelScope)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,7 @@ class TopicsViewModelFactory() : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>) =
|
override fun <T : ViewModel?> create(modelClass: Class<T>) =
|
||||||
with(modelClass){
|
with(modelClass){
|
||||||
when {
|
when {
|
||||||
isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(TopicsRepository.getInstance()) as T
|
isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(Repository.getInstance()) as T
|
||||||
else -> throw IllegalArgumentException("Unknown viewModel class $modelClass")
|
else -> throw IllegalArgumentException("Unknown viewModel class $modelClass")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
/*
|
|
||||||
* 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.add
|
package io.heckel.ntfy.add
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
@ -23,10 +7,12 @@ import android.widget.Button
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.TOPIC_URL
|
import io.heckel.ntfy.TOPIC_BASE_URL
|
||||||
|
import io.heckel.ntfy.TOPIC_NAME
|
||||||
|
|
||||||
class AddTopicActivity : AppCompatActivity() {
|
class AddTopicActivity : AppCompatActivity() {
|
||||||
private lateinit var addTopicUrl: TextInputEditText
|
private lateinit var topicName: TextInputEditText
|
||||||
|
private lateinit var baseUrl: TextInputEditText
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -35,8 +21,9 @@ class AddTopicActivity : AppCompatActivity() {
|
||||||
findViewById<Button>(R.id.subscribe_button).setOnClickListener {
|
findViewById<Button>(R.id.subscribe_button).setOnClickListener {
|
||||||
addTopic()
|
addTopic()
|
||||||
}
|
}
|
||||||
addTopicUrl = findViewById(R.id.add_topic_url)
|
topicName = findViewById(R.id.add_topic_name)
|
||||||
addTopicUrl.setText("https://ntfy.sh/")
|
baseUrl = findViewById(R.id.add_topic_base_url)
|
||||||
|
baseUrl.setText(R.string.topic_base_url_default_value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The onClick action for the done button. Closes the activity and returns the new topic name
|
/* The onClick action for the done button. Closes the activity and returns the new topic name
|
||||||
|
@ -46,11 +33,13 @@ class AddTopicActivity : AppCompatActivity() {
|
||||||
private fun addTopic() {
|
private fun addTopic() {
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
|
|
||||||
if (addTopicUrl.text.isNullOrEmpty()) {
|
// TODO don't allow this
|
||||||
|
|
||||||
|
if (baseUrl.text.isNullOrEmpty()) {
|
||||||
setResult(Activity.RESULT_CANCELED, resultIntent)
|
setResult(Activity.RESULT_CANCELED, resultIntent)
|
||||||
} else {
|
} else {
|
||||||
val url = addTopicUrl.text.toString()
|
resultIntent.putExtra(TOPIC_NAME, topicName.text.toString())
|
||||||
resultIntent.putExtra(TOPIC_URL, url)
|
resultIntent.putExtra(TOPIC_BASE_URL, baseUrl.text.toString())
|
||||||
setResult(Activity.RESULT_OK, resultIntent)
|
setResult(Activity.RESULT_OK, resultIntent)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
|
|
|
@ -12,8 +12,8 @@ import java.io.IOException
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
/* Handles operations on topicsLiveData and holds details about it. */
|
class Repository {
|
||||||
class TopicsRepository {
|
private val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed
|
||||||
private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
|
private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
|
||||||
private val jobs = mutableMapOf<Long, Job>()
|
private val jobs = mutableMapOf<Long, Job>()
|
||||||
private val gson = GsonBuilder().create()
|
private val gson = GsonBuilder().create()
|
||||||
|
@ -62,33 +62,38 @@ class TopicsRepository {
|
||||||
private fun subscribeTopic(topic: Topic, scope: CoroutineScope): Job {
|
private fun subscribeTopic(topic: Topic, scope: CoroutineScope): Job {
|
||||||
return scope.launch(Dispatchers.IO) {
|
return scope.launch(Dispatchers.IO) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
openURL(this, topic.url, topic.url) // TODO
|
openConnection(this, topic)
|
||||||
delay(5000) // TODO exponential back-off
|
delay(5000) // TODO exponential back-off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openURL(scope: CoroutineScope, topic: String, url: String) {
|
private fun openConnection(scope: CoroutineScope, topic: Topic) {
|
||||||
|
val url = "${topic.baseUrl}/${topic.name}/json"
|
||||||
println("Connecting to $url ...")
|
println("Connecting to $url ...")
|
||||||
val conn = (URL(url).openConnection() as HttpURLConnection).also {
|
val conn = (URL(url).openConnection() as HttpURLConnection).also {
|
||||||
it.doInput = true
|
it.doInput = true
|
||||||
|
it.readTimeout = READ_TIMEOUT
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val input = conn.inputStream.bufferedReader()
|
val input = conn.inputStream.bufferedReader()
|
||||||
while (scope.isActive) {
|
while (scope.isActive) {
|
||||||
val line = input.readLine() ?: break // Exit if null
|
val line = input.readLine() ?: break // Break if EOF is reached, i.e. readLine is null
|
||||||
|
if (!scope.isActive) {
|
||||||
|
break // Break if scope is not active anymore; readLine blocks for a while, so we want to be sure
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val json = gson.fromJson(line, JsonObject::class.java) ?: break // Exit if null
|
val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line
|
||||||
if (!json.isJsonNull && json.has("message")) {
|
if (!json.isJsonNull && json.has("message")) {
|
||||||
val message = json.get("message").asString
|
val message = json.get("message").asString
|
||||||
notificationListener?.let { it(Notification(url, message)) }
|
notificationListener?.let { it(Notification(topic.name, message)) }
|
||||||
}
|
}
|
||||||
} catch (e: JsonSyntaxException) {
|
} catch (e: JsonSyntaxException) {
|
||||||
// Ignore invalid JSON
|
break // Break on unexpected line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
println("PHIL: " + e.message)
|
println("Connection error: " + e.message)
|
||||||
} finally {
|
} finally {
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
|
@ -96,11 +101,11 @@ class TopicsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: TopicsRepository? = null
|
private var instance: Repository? = null
|
||||||
|
|
||||||
fun getInstance(): TopicsRepository {
|
fun getInstance(): Repository {
|
||||||
return synchronized(TopicsRepository::class) {
|
return synchronized(Repository::class) {
|
||||||
val newInstance = instance ?: TopicsRepository()
|
val newInstance = instance ?: Repository()
|
||||||
instance = newInstance
|
instance = newInstance
|
||||||
newInstance
|
newInstance
|
||||||
}
|
}
|
|
@ -1,22 +1,7 @@
|
||||||
/*
|
|
||||||
* 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.data
|
package io.heckel.ntfy.data
|
||||||
|
|
||||||
data class Topic(
|
data class Topic(
|
||||||
val id: Long,
|
val id: Long, // Internal to Repository only
|
||||||
val url: String,
|
val name: String,
|
||||||
|
val baseUrl: String,
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,30 +35,31 @@ class DetailActivity : AppCompatActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.topic_detail_activity)
|
setContentView(R.layout.topic_detail_activity)
|
||||||
|
|
||||||
var currentTopicId: Long? = null
|
var topicId: Long? = null
|
||||||
|
|
||||||
/* Connect variables to UI elements. */
|
/* Connect variables to UI elements. */
|
||||||
val topicUrl: TextView = findViewById(R.id.topic_detail_url)
|
val topicText: TextView = findViewById(R.id.topic_detail_url)
|
||||||
val removeTopicButton: Button = findViewById(R.id.remove_button)
|
val removeButton: Button = findViewById(R.id.remove_button)
|
||||||
|
|
||||||
val bundle: Bundle? = intent.extras
|
val bundle: Bundle? = intent.extras
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
currentTopicId = bundle.getLong(TOPIC_ID)
|
topicId = bundle.getLong(TOPIC_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO This should probably fail hard if topicId is null
|
||||||
|
|
||||||
/* If currentTopicId is not null, get corresponding topic and set name, image and
|
/* If currentTopicId is not null, get corresponding topic and set name, image and
|
||||||
description */
|
description */
|
||||||
currentTopicId?.let {
|
topicId?.let {
|
||||||
val currentTopic = topicsViewModel.get(it)
|
val topic = topicsViewModel.get(it)
|
||||||
topicUrl.text = currentTopic?.url
|
topicText.text = "${topic?.baseUrl}/${topic?.name}"
|
||||||
|
|
||||||
removeTopicButton.setOnClickListener {
|
removeButton.setOnClickListener {
|
||||||
if (currentTopic != null) {
|
if (topic != null) {
|
||||||
topicsViewModel.remove(currentTopic)
|
topicsViewModel.remove(topic)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,27 +20,40 @@
|
||||||
android:paddingRight="16dp">
|
android:paddingRight="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
android:text="@string/add_topic"
|
android:text="@string/add_topic"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="?attr/textAppearanceHeadline4" />
|
android:textAppearance="?attr/textAppearanceHeadline4"/>
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:hint="@string/topic_name_edit_text"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/add_topic_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:hint="@string/topic_name_edit_text"
|
android:hint="@string/topic_base_url_edit_text"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="16dp">
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/add_topic_url"
|
android:id="@+id/add_topic_base_url"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<TextView
|
<TextView
|
||||||
android:text="https://ntfy.sh/example"
|
android:text="ntfy.sh/example"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/topic_text"
|
android:layout_height="wrap_content" android:id="@+id/topic_text"
|
||||||
android:layout_marginTop="16dp" android:layout_marginLeft="16dp"
|
android:layout_marginTop="16dp" android:layout_marginLeft="16dp"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="Subscribed, 0 messages"
|
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_marginLeft="16dp"/>
|
||||||
|
|
|
@ -1,25 +1,12 @@
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Ntfy</string>
|
<string name="app_name">Ntfy</string>
|
||||||
<string name="add_topic">Add Topic</string>
|
<string name="add_topic">Add Topic</string>
|
||||||
|
|
||||||
<string name="topic_string">Topics</string>
|
<string name="topic_string">Topics</string>
|
||||||
<string name="topic_name_edit_text">Topic URL</string>
|
<string name="topic_name_edit_text">Topic Name</string>
|
||||||
|
<string name="topic_base_url_edit_text">Service URL</string>
|
||||||
|
<string name="topic_base_url_default_value">https://ntfy.sh</string>
|
||||||
|
<string name="topic_short_name_format">%1$s/%2$s</string>
|
||||||
<string name="subscribe_button_text">Subscribe</string>
|
<string name="subscribe_button_text">Subscribe</string>
|
||||||
<string name="fab_content_description">fab</string>
|
<string name="fab_content_description">fab</string>
|
||||||
<string name="remove_topic">Unsubscribe</string>
|
<string name="remove_topic">Unsubscribe</string>
|
||||||
|
|
Loading…
Reference in a new issue