Compare commits
8 commits
bfa2285607
...
faf841824d
Author | SHA1 | Date | |
---|---|---|---|
dece | faf841824d | ||
dece | 6e380290b4 | ||
dece | 12c3a9e4bb | ||
dece | 6381e40208 | ||
dece | 4449e56dba | ||
dece | e5df22e8b5 | ||
dece | dd2fd3812d | ||
dece | 34ba7cc731 |
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -40,10 +41,12 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation "androidx.room:room-runtime:2.3.0"
|
||||||
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
|
kapt "androidx.room:room-compiler:2.3.0"
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
21
app/src/main/java/dev/lowrespalmtree/comet/Database.kt
Normal file
21
app/src/main/java/dev/lowrespalmtree/comet/Database.kt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
|
@Database(entities = [History.HistoryEntry::class], version = 1)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun historyEntryDao(): History.HistoryEntryDao
|
||||||
|
}
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
lateinit var INSTANCE: AppDatabase
|
||||||
|
|
||||||
|
fun init(context: Context) {
|
||||||
|
if (::INSTANCE.isInitialized)
|
||||||
|
return
|
||||||
|
INSTANCE = Room.databaseBuilder(context, AppDatabase::class.java, "comet").build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,9 +55,10 @@ private fun parseLine(line: CharBuffer, isPreformatted: Boolean): Line =
|
||||||
line.startsWith(">") -> BlockquoteLine(getCharsFrom(line, 1))
|
line.startsWith(">") -> BlockquoteLine(getCharsFrom(line, 1))
|
||||||
line.startsWith("```") -> PreFenceLine(getCharsFrom(line, 3))
|
line.startsWith("```") -> PreFenceLine(getCharsFrom(line, 3))
|
||||||
line.startsWith("* ") -> ListItemLine(getCharsFrom(line, 2))
|
line.startsWith("* ") -> ListItemLine(getCharsFrom(line, 2))
|
||||||
line.startsWith("=>") -> getCharsFrom(line, 2).split(" ", limit = 2)
|
line.startsWith("=>") -> getCharsFrom(line, 2)
|
||||||
.run { LinkLine(get(0), if (size == 2) get(1) else "") }
|
.split(" ", limit = 2)
|
||||||
|
.run { LinkLine(get(0), if (size == 2) get(1).trimStart() else "") }
|
||||||
else -> if (isPreformatted) PreTextLine(line.toString()) else ParagraphLine(line.toString())
|
else -> if (isPreformatted) PreTextLine(line.toString()) else ParagraphLine(line.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCharsFrom(line: CharBuffer, index: Int) = line.substring(index).removePrefix(" ")
|
private fun getCharsFrom(line: CharBuffer, index: Int) = line.substring(index).trim()
|
37
app/src/main/java/dev/lowrespalmtree/comet/History.kt
Normal file
37
app/src/main/java/dev/lowrespalmtree/comet/History.kt
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
object History {
|
||||||
|
@Entity
|
||||||
|
data class HistoryEntry(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "uri") val uri: String,
|
||||||
|
@ColumnInfo(name = "title") var title: String?,
|
||||||
|
@ColumnInfo(name = "lastVisit") var lastVisit: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface HistoryEntryDao {
|
||||||
|
@Query("SELECT * FROM HistoryEntry WHERE :uri = uri LIMIT 1")
|
||||||
|
fun get(uri: String): HistoryEntry?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM HistoryEntry ORDER BY lastVisit DESC")
|
||||||
|
fun getAll(): List<HistoryEntry>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insert(vararg entries: HistoryEntry)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(vararg entries: HistoryEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun record(uri: String, title: String?) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val dao = Database.INSTANCE.historyEntryDao()
|
||||||
|
val entry = dao.get(uri)
|
||||||
|
if (entry == null)
|
||||||
|
dao.insert(HistoryEntry(uri, title, now))
|
||||||
|
else
|
||||||
|
dao.update(entry.also { it.title = title; it.lastVisit = now })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package dev.lowrespalmtree.comet
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -18,9 +17,8 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dev.lowrespalmtree.comet.databinding.ActivityMainBinding
|
import dev.lowrespalmtree.comet.databinding.ActivityMainBinding
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
import java.net.ConnectException
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
@ -29,11 +27,19 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
private lateinit var pageViewModel: PageViewModel
|
private lateinit var pageViewModel: PageViewModel
|
||||||
private lateinit var adapter: ContentAdapter
|
private lateinit var adapter: ContentAdapter
|
||||||
|
|
||||||
private val currentUrl get() = binding.addressBar.text
|
/** Property to access and set the current address bar URL value. */
|
||||||
|
private var currentUrl
|
||||||
|
get() = binding.addressBar.text.toString()
|
||||||
|
set(value) = binding.addressBar.setText(value)
|
||||||
|
|
||||||
|
/** A non-saved list of visited URLs. Not an history, just used for going back. */
|
||||||
|
private val visitedUrls = mutableListOf<String>()
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
Database.init(applicationContext)
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
@ -54,19 +60,32 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.contentSwipeLayout.setOnRefreshListener { openUrl(currentUrl.toString()) }
|
binding.contentSwipeLayout.setOnRefreshListener { openUrl(currentUrl) }
|
||||||
|
|
||||||
pageViewModel.state.observe(this, { updateState(it) })
|
pageViewModel.state.observe(this, { updateState(it) })
|
||||||
pageViewModel.lines.observe(this, { updateLines(it) })
|
pageViewModel.lines.observe(this, { updateLines(it) })
|
||||||
pageViewModel.alert.observe(this, { alert(it) })
|
pageViewModel.event.observe(this, { handleEvent(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
visitedUrls.removeLastOrNull() // Always remove current page first.
|
||||||
|
val previousUrl = visitedUrls.removeLastOrNull()
|
||||||
|
if (previousUrl != null)
|
||||||
|
openUrl(previousUrl)
|
||||||
|
else
|
||||||
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLinkClick(url: String) {
|
override fun onLinkClick(url: String) {
|
||||||
val base = binding.addressBar.text.toString()
|
openUrl(url, base = if (currentUrl.isNotEmpty()) currentUrl else null)
|
||||||
openUrl(url, base = if (base.isNotEmpty()) base else null)
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String, base: String? = null, redirections: Int = 0) {
|
||||||
|
if (redirections >= 5) {
|
||||||
|
alert("Too many redirections.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openUrl(url: String, base: String? = null) {
|
|
||||||
var uri = Uri.parse(url)
|
var uri = Uri.parse(url)
|
||||||
if (!uri.isAbsolute) {
|
if (!uri.isAbsolute) {
|
||||||
uri = if (!base.isNullOrEmpty()) joinUrls(base, url) else toGeminiUri(uri)
|
uri = if (!base.isNullOrEmpty()) joinUrls(base, url) else toGeminiUri(uri)
|
||||||
|
@ -74,7 +93,7 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
|
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"gemini" -> {
|
"gemini" -> {
|
||||||
binding.addressBar.setText(uri.toString())
|
currentUrl = uri.toString()
|
||||||
pageViewModel.sendGeminiRequest(uri)
|
pageViewModel.sendGeminiRequest(uri)
|
||||||
}
|
}
|
||||||
else -> openUnknownScheme(uri)
|
else -> openUnknownScheme(uri)
|
||||||
|
@ -102,13 +121,21 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
adapter.setContent(lines)
|
adapter.setContent(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun alert(message: String, title: String? = null) {
|
private fun handleEvent(event: PageViewModel.Event) {
|
||||||
val builder = AlertDialog.Builder(this)
|
Log.d(TAG, "handleEvent: $event")
|
||||||
if (title != null)
|
if (!event.handled) {
|
||||||
builder.setTitle(title)
|
when (event) {
|
||||||
else
|
is PageViewModel.SuccessEvent -> visitedUrls.add(event.uri)
|
||||||
builder.setTitle(title ?: R.string.alert_title)
|
is PageViewModel.RedirectEvent -> openUrl(event.uri, redirections = event.redirects)
|
||||||
builder
|
is PageViewModel.FailureEvent -> alert(event.message)
|
||||||
|
}
|
||||||
|
event.handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun alert(message: String) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.error_alert_title)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.create()
|
.create()
|
||||||
.show()
|
.show()
|
||||||
|
@ -127,18 +154,23 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) }
|
val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) }
|
||||||
private var linesList = ArrayList<Line>()
|
private var linesList = ArrayList<Line>()
|
||||||
val lines: MutableLiveData<List<Line>> by lazy { MutableLiveData<List<Line>>() }
|
val lines: MutableLiveData<List<Line>> by lazy { MutableLiveData<List<Line>>() }
|
||||||
val alert: MutableLiveData<String> by lazy { MutableLiveData<String>() }
|
val event: MutableLiveData<Event> by lazy { MutableLiveData<Event>() }
|
||||||
|
|
||||||
enum class State {
|
enum class State {
|
||||||
IDLE, CONNECTING, RECEIVING
|
IDLE, CONNECTING, RECEIVING
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class Event(var handled: Boolean = false)
|
||||||
|
data class SuccessEvent(val uri: String) : Event()
|
||||||
|
data class RedirectEvent(val uri: String, val redirects: Int) : Event()
|
||||||
|
data class FailureEvent(val message: String) : Event()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a request against this URI.
|
* Perform a request against this URI.
|
||||||
*
|
*
|
||||||
* The URI must be valid, absolute and with a gemini scheme.
|
* The URI must be valid, absolute and with a gemini scheme.
|
||||||
*/
|
*/
|
||||||
fun sendGeminiRequest(uri: Uri) {
|
fun sendGeminiRequest(uri: Uri, redirects: Int = 0) {
|
||||||
Log.d(TAG, "sendRequest: URI \"$uri\"")
|
Log.d(TAG, "sendRequest: URI \"$uri\"")
|
||||||
state.postValue(State.CONNECTING)
|
state.postValue(State.CONNECTING)
|
||||||
requestJob?.apply { if (isActive) cancel() }
|
requestJob?.apply { if (isActive) cancel() }
|
||||||
|
@ -148,40 +180,54 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
val socket = request.connect()
|
val socket = request.connect()
|
||||||
val channel = request.proceed(socket, this)
|
val channel = request.proceed(socket, this)
|
||||||
Response.from(channel, viewModelScope)
|
Response.from(channel, viewModelScope)
|
||||||
} catch (e: UnknownHostException) {
|
|
||||||
signalError("Unknown host \"${uri.authority}\".")
|
|
||||||
return@launch
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "sendGeminiRequest coroutine: ${e.stackTraceToString()}")
|
Log.e(TAG, "sendGeminiRequest coroutine: ${e.stackTraceToString()}")
|
||||||
signalError("Oops! Whatever we tried to do failed!")
|
// If we got cancelled, die silently.
|
||||||
|
if (!isActive)
|
||||||
|
return@launch
|
||||||
|
signalError(
|
||||||
|
when (e) {
|
||||||
|
is UnknownHostException -> "Unknown host \"${uri.authority}\"."
|
||||||
|
is ConnectException -> "Can't connect to this server: ${e.message}."
|
||||||
|
else -> "Oops, something failed!"
|
||||||
|
}
|
||||||
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
if (!isActive)
|
||||||
|
return@launch
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
signalError("Can't parse server response.")
|
signalError("Can't parse server response.")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "sendRequest: got ${response.code} with meta \"${response.meta}\"")
|
Log.i(TAG, "sendRequest: got ${response.code} with meta \"${response.meta}\"")
|
||||||
when (response.code) {
|
when (response.code.getCategory()) {
|
||||||
Response.Code.SUCCESS -> handleRequestSuccess(response)
|
Response.Code.Category.SUCCESS -> handleRequestSuccess(response, uri)
|
||||||
|
Response.Code.Category.REDIRECT -> handleRedirect(response, redirects = redirects + 1)
|
||||||
|
Response.Code.Category.SERVER_ERROR -> handleError(response)
|
||||||
else -> signalError("Can't handle code ${response.code}.")
|
else -> signalError("Can't handle code ${response.code}.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signalError(message: String) {
|
private fun signalError(message: String) {
|
||||||
alert.postValue(message)
|
event.postValue(FailureEvent(message))
|
||||||
state.postValue(State.IDLE)
|
state.postValue(State.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleRequestSuccess(response: Response) {
|
private suspend fun handleRequestSuccess(response: Response, uri: Uri) {
|
||||||
state.postValue(State.RECEIVING)
|
state.postValue(State.RECEIVING)
|
||||||
|
|
||||||
linesList.clear()
|
linesList.clear()
|
||||||
lines.postValue(linesList)
|
lines.postValue(linesList)
|
||||||
val charset = Charset.defaultCharset()
|
val charset = Charset.defaultCharset()
|
||||||
|
var mainTitle: String? = null
|
||||||
var lastUpdate = System.currentTimeMillis()
|
var lastUpdate = System.currentTimeMillis()
|
||||||
for (line in parseData(response.data, charset, viewModelScope)) {
|
for (line in parseData(response.data, charset, viewModelScope)) {
|
||||||
linesList.add(line)
|
linesList.add(line)
|
||||||
|
if (mainTitle == null && line is TitleLine && line.level == 1)
|
||||||
|
mainTitle = line.text
|
||||||
val time = System.currentTimeMillis()
|
val time = System.currentTimeMillis()
|
||||||
if (time - lastUpdate >= 100) { // Throttle to 100ms the recycler view updates…
|
if (time - lastUpdate >= 100) { // Throttle to 100ms the recycler view updates…
|
||||||
lines.postValue(linesList)
|
lines.postValue(linesList)
|
||||||
|
@ -189,8 +235,33 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.postValue(linesList)
|
lines.postValue(linesList)
|
||||||
|
|
||||||
|
// We record the history entry here: it's nice because we have the main title available
|
||||||
|
// and we're already in a coroutine for database access.
|
||||||
|
History.record(uri.toString(), mainTitle)
|
||||||
|
event.postValue(SuccessEvent(uri.toString()))
|
||||||
state.postValue(State.IDLE)
|
state.postValue(State.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleRedirect(response: Response, redirects: Int) {
|
||||||
|
event.postValue(RedirectEvent(response.meta, redirects))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(response: Response) {
|
||||||
|
event.postValue(FailureEvent(when (response.code) {
|
||||||
|
Response.Code.TEMPORARY_FAILURE -> "40: the server encountered a temporary failure."
|
||||||
|
Response.Code.SERVER_UNAVAILABLE -> "41: the server is currently unavailable."
|
||||||
|
Response.Code.CGI_ERROR -> "42: a CGI script encountered an error."
|
||||||
|
Response.Code.PROXY_ERROR -> "43: the server failed to proxy the request."
|
||||||
|
Response.Code.SLOW_DOWN -> "44: slow down!"
|
||||||
|
Response.Code.PERMANENT_FAILURE -> "50: this request failed and similar requests will likely fail as well."
|
||||||
|
Response.Code.NOT_FOUND -> "51: this page can't be found."
|
||||||
|
Response.Code.GONE -> "52: this page is gone."
|
||||||
|
Response.Code.PROXY_REQUEST_REFUSED -> "53: the server refused to proxy the request."
|
||||||
|
Response.Code.BAD_REQUEST -> "59: bad request."
|
||||||
|
else -> "${response.code}: unknown error code."
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -31,6 +31,18 @@ class Response(val code: Code, val meta: String, val data: Channel<ByteArray>) {
|
||||||
CERTIFICATE_NOT_AUTHORISED(61),
|
CERTIFICATE_NOT_AUTHORISED(61),
|
||||||
CERTIFICATE_NOT_VALID(62);
|
CERTIFICATE_NOT_VALID(62);
|
||||||
|
|
||||||
|
enum class Category(val value: Int) {
|
||||||
|
UNKNOWN(0),
|
||||||
|
INPUT(1),
|
||||||
|
SUCCESS(2),
|
||||||
|
REDIRECT(3),
|
||||||
|
SERVER_ERROR(4),
|
||||||
|
CLIENT_ERROR(5),
|
||||||
|
CERTIFICATE(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategory(): Category? = Category.values().associateBy(Category::value)[value / 10]
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val MAP = values().associateBy(Code::value)
|
private val MAP = values().associateBy(Code::value)
|
||||||
fun fromInt(type: Int) = MAP[type]
|
fun fromInt(type: Int) = MAP[type]
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="2dp"
|
||||||
android:paddingBottom="4dp" />
|
android:paddingBottom="4dp" />
|
|
@ -7,6 +7,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="2dp"
|
||||||
android:paddingBottom="4dp"
|
android:paddingBottom="4dp"
|
||||||
android:textIsSelectable="false" />
|
android:textIsSelectable="false" />
|
|
@ -8,4 +8,4 @@
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="2dp"
|
android:paddingTop="2dp"
|
||||||
android:paddingBottom="2dp" />
|
android:paddingBottom="4dp" />
|
|
@ -7,5 +7,5 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="2dp"
|
||||||
android:paddingBottom="4dp" />
|
android:paddingBottom="4dp" />
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Comet</string>
|
<string name="app_name">Comet</string>
|
||||||
<string name="alert_title">Alert</string>
|
<string name="error_alert_title">Error</string>
|
||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="url">URL</string>
|
<string name="url">URL</string>
|
||||||
</resources>
|
</resources>
|
Reference in a new issue