Compare commits

...

2 Commits

@ -45,18 +45,18 @@ android {
}
dependencies {
def nav_version = "2.3.5"
def nav_version = "2.4.0"
def room_version = "2.4.1"
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"

@ -13,27 +13,36 @@ class UriUtilsTest {
fun joinUrls() {
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/", "some-file.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls("gemini://dece.space/", "some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/", "./some-file.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls("gemini://dece.space/", "./some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/dir1", "/some-file.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls("gemini://dece.space/dir1", "/some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/dir1/other-file.gmi",
joinUrls("gemini://dece.space/dir1/file.gmi", "other-file.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls(
"gemini://dece.space/dir1/file.gmi",
"other-file.gmi"
).toString()
)
assertEquals(
"gemini://dece.space/top-level.gmi",
joinUrls("gemini://dece.space/dir1/file.gmi", "../top-level.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls(
"gemini://dece.space/dir1/file.gmi",
"../top-level.gmi"
).toString()
)
assertEquals(
"s://hard/test/b/d/a.gmi",
joinUrls("s://hard/dir/a", "./../test/b/c/../d/e/f/../.././a.gmi").toString()
dev.lowrespalmtree.comet.utils.joinUrls(
"s://hard/dir/a",
"./../test/b/c/../d/e/f/../.././a.gmi"
).toString()
)
}
@ -49,28 +58,28 @@ class UriUtilsTest {
Pair("mid/content=5/../6", "mid/6"),
Pair("../../../../g", "g")
).forEach { (path, expected) ->
assertEquals(expected, removeDotSegments(path))
assertEquals(expected, dev.lowrespalmtree.comet.utils.removeDotSegments(path))
}
}
@Test
fun removeLastSegment() {
assertEquals("", removeLastSegment(""))
assertEquals("", removeLastSegment("/"))
assertEquals("", removeLastSegment("/a"))
assertEquals("/a", removeLastSegment("/a/"))
assertEquals("/a", removeLastSegment("/a/b"))
assertEquals("/a/b/c", removeLastSegment("/a/b/c/d"))
assertEquals("//", removeLastSegment("///"))
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment(""))
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment("/"))
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment("/a"))
assertEquals("/a", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/"))
assertEquals("/a", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/b"))
assertEquals("/a/b/c", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/b/c/d"))
assertEquals("//", dev.lowrespalmtree.comet.utils.removeLastSegment("///"))
}
@Test
fun popFirstSegment() {
assertEquals(Pair("", ""), popFirstSegment(""))
assertEquals(Pair("a", ""), popFirstSegment("a"))
assertEquals(Pair("/a", ""), popFirstSegment("/a"))
assertEquals(Pair("/a", "/"), popFirstSegment("/a/"))
assertEquals(Pair("/a", "/b"), popFirstSegment("/a/b"))
assertEquals(Pair("a", "/b"), popFirstSegment("a/b"))
assertEquals(Pair("", ""), dev.lowrespalmtree.comet.utils.popFirstSegment(""))
assertEquals(Pair("a", ""), dev.lowrespalmtree.comet.utils.popFirstSegment("a"))
assertEquals(Pair("/a", ""), dev.lowrespalmtree.comet.utils.popFirstSegment("/a"))
assertEquals(Pair("/a", "/"), dev.lowrespalmtree.comet.utils.popFirstSegment("/a/"))
assertEquals(Pair("/a", "/b"), dev.lowrespalmtree.comet.utils.popFirstSegment("/a/b"))
assertEquals(Pair("a", "/b"), dev.lowrespalmtree.comet.utils.popFirstSegment("a/b"))
}
}

@ -1,7 +1,6 @@
package dev.lowrespalmtree.comet
import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@ -7,12 +7,13 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import dev.lowrespalmtree.comet.History.HistoryEntry
import dev.lowrespalmtree.comet.databinding.FragmentHistoryItemBinding
import dev.lowrespalmtree.comet.utils.getFancySelectBgRes
class HistoryAdapter(private val listener: HistoryItemAdapterListener) :
class HistoryAdapter(private val listener: Listener) :
RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
private var items = listOf<HistoryEntry>()
interface HistoryItemAdapterListener {
interface Listener {
fun onItemClick(url: String)
}
@ -31,7 +32,7 @@ class HistoryAdapter(private val listener: HistoryItemAdapterListener) :
holder.binding.titleText.visibility =
if (item.title.isNullOrBlank()) View.GONE else View.VISIBLE
holder.binding.titleText.text = item.title ?: ""
holder.binding.card.setOnClickListener { listener.onItemClick(item.uri) }
holder.binding.container.setOnClickListener { listener.onItemClick(item.uri) }
}
override fun getItemCount(): Int = items.size
@ -43,5 +44,9 @@ class HistoryAdapter(private val listener: HistoryItemAdapterListener) :
}
inner class ViewHolder(val binding: FragmentHistoryItemBinding) :
RecyclerView.ViewHolder(binding.root)
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setBackgroundResource(getFancySelectBgRes(itemView.context))
}
}
}

@ -12,16 +12,14 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lowrespalmtree.comet.History.HistoryEntry
import dev.lowrespalmtree.comet.HistoryAdapter.HistoryItemAdapterListener
import dev.lowrespalmtree.comet.databinding.FragmentHistoryListBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
class HistoryFragment : Fragment(), HistoryItemAdapterListener {
class HistoryFragment : Fragment(), HistoryAdapter.Listener {
private val vm: HistoryViewModel by viewModels()
private lateinit var binding: FragmentHistoryListBinding
private lateinit var adapter: HistoryAdapter
@ -36,11 +34,13 @@ class HistoryFragment : Fragment(), HistoryItemAdapterListener {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val lm = LinearLayoutManager(requireContext())
binding.list.layoutManager = lm
binding.list.addItemDecoration(DividerItemDecoration(context, lm.orientation))
adapter = HistoryAdapter(this)
binding.list.layoutManager = LinearLayoutManager(requireContext())
binding.list.adapter = adapter
vm.items.observe(viewLifecycleOwner, { adapter.setItems(it) })
vm.items.observe(viewLifecycleOwner) { adapter.setItems(it) }
vm.refreshHistory()
}
@ -50,7 +50,6 @@ class HistoryFragment : Fragment(), HistoryItemAdapterListener {
findNavController().navigate(R.id.action_global_pageFragment, bundle)
}
@ExperimentalCoroutinesApi
class HistoryViewModel(
@Suppress("unused") private val savedStateHandle: SavedStateHandle
) : ViewModel() {

@ -1,6 +1,10 @@
package dev.lowrespalmtree.comet
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.room.*
import java.security.KeyPairGenerator
object Identities {
@Entity
@ -10,7 +14,7 @@ object Identities {
/** Key to retrieve certificate from the keystore. */
val key: String,
/** Label for this identity. */
val name: String?,
var name: String?,
)
@Entity
@ -26,13 +30,44 @@ object Identities {
@Dao
interface IdentityDao {
@Insert
suspend fun insert(vararg entries: Identity)
suspend fun insert(identity: Identity): Long
@Query("SELECT * FROM Identity WHERE :id = id")
suspend fun get(id: Long): Identity?
@Query("SELECT * FROM Identity ORDER BY id")
suspend fun getAll(): List<Identity>
@Update
suspend fun update(vararg identities: Identity)
@Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId")
fun getUsagesFor(identityId: Int): List<IdentityUsage>
suspend fun getUsagesFor(identityId: Int): List<IdentityUsage>
}
suspend fun insert(key: String, name: String? = null) {
suspend fun insert(key: String, name: String? = null): Long =
Database.INSTANCE.identityDao().insert(Identity(0, key, name))
suspend fun get(id: Long): Identity? =
Database.INSTANCE.identityDao().get(id)
suspend fun getAll(): List<Identity> =
Database.INSTANCE.identityDao().getAll()
suspend fun update(vararg identities: Identity) =
Database.INSTANCE.identityDao().update(*identities)
fun generateClientCert(alias: String) {
val algo = KeyProperties.KEY_ALGORITHM_RSA
val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore")
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val spec = KeyGenParameterSpec.Builder(alias, purposes)
.setDigests(KeyProperties.DIGEST_SHA256)
.build()
kpg.initialize(spec)
kpg.generateKeyPair()
Log.i(TAG, "generateClientCert: key pair with alias \"$alias\" has been generated")
}
private const val TAG = "Identities"
}

@ -1,24 +1,49 @@
package dev.lowrespalmtree.comet
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import dev.lowrespalmtree.comet.Identities.Identity
import dev.lowrespalmtree.comet.databinding.FragmentIdentityBinding
import dev.lowrespalmtree.comet.utils.getFancySelectBgRes
class IdentitiesAdapter : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
class IdentitiesAdapter(private val listener: Listener) :
RecyclerView.Adapter<IdentitiesAdapter.ViewHolder>() {
private var identities = listOf<Identity>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryAdapter.ViewHolder {
TODO("Not yet implemented")
interface Listener {
fun onIdentityClick(identity: Identity)
}
override fun onBindViewHolder(holder: HistoryAdapter.ViewHolder, position: Int) {
TODO("Not yet implemented")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
FragmentIdentityBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = identities[position]
holder.binding.labelText.text = item.name.orEmpty()
holder.binding.keyText.text = item.key
holder.binding.container.setOnClickListener { listener.onIdentityClick(item) }
}
override fun getItemCount(): Int {
TODO("Not yet implemented")
override fun getItemCount(): Int = identities.size
@SuppressLint("NotifyDataSetChanged")
fun setIdentities(newIdentities: List<Identity>) {
identities = newIdentities.toList()
notifyDataSetChanged()
}
inner class ViewHolder(val binding: FragmentIdentityBinding) : RecyclerView.ViewHolder(binding.root)
inner class ViewHolder(val binding: FragmentIdentityBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setBackgroundResource(getFancySelectBgRes(itemView.context))
}
}
}

@ -1,14 +1,27 @@
package dev.lowrespalmtree.comet
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lowrespalmtree.comet.Identities.Identity
import dev.lowrespalmtree.comet.databinding.FragmentIdentitiesBinding
import dev.lowrespalmtree.comet.utils.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
class IdentitiesFragment : Fragment() {
class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialog.Listener {
private val vm: IdentitiesViewModel by viewModels()
private lateinit var binding: FragmentIdentitiesBinding
private lateinit var adapter: IdentitiesAdapter
override fun onCreateView(
inflater: LayoutInflater,
@ -18,4 +31,59 @@ class IdentitiesFragment : Fragment() {
binding = FragmentIdentitiesBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val lm = LinearLayoutManager(requireContext())
binding.list.layoutManager = lm
binding.list.addItemDecoration(DividerItemDecoration(context, lm.orientation))
adapter = IdentitiesAdapter(this)
binding.list.adapter = adapter
binding.floatingActionButton.setOnClickListener { openNewIdentityEditor() }
vm.identities.observe(viewLifecycleOwner) { adapter.setIdentities(it) }
vm.refreshIdentities()
}
override fun onIdentityClick(identity: Identity) {
IdentityDialog(requireContext(), identity, this).show()
}
override fun onSaveIdentity(identity: Identity) {
vm.saveIdentity(identity)
}
private fun openNewIdentityEditor() {
toast(requireContext(), R.string.generating_keypair)
vm.newIdentity.observe(viewLifecycleOwner) { identity ->
vm.newIdentity.removeObservers(viewLifecycleOwner)
IdentityDialog(requireContext(), identity, this).show()
}
vm.createNewIdentity()
}
class IdentitiesViewModel : ViewModel() {
val identities: MutableLiveData<List<Identity>> by lazy { MutableLiveData<List<Identity>>() }
val newIdentity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() }
fun createNewIdentity() {
viewModelScope.launch(Dispatchers.IO) {
val alias = "identity-${UUID.randomUUID()}"
Identities.generateClientCert(alias)
val newIdentityId = Identities.insert(alias)
newIdentity.postValue(Identities.get(newIdentityId))
}
}
fun refreshIdentities() {
viewModelScope.launch(Dispatchers.IO) {
identities.postValue(Identities.getAll())
}
}
fun saveIdentity(identity: Identity) {
viewModelScope.launch(Dispatchers.IO) { Identities.update(identity) }
}
}
}

@ -0,0 +1,33 @@
package dev.lowrespalmtree.comet
import android.app.AlertDialog
import android.content.Context
import android.view.LayoutInflater
import dev.lowrespalmtree.comet.databinding.DialogIdentityBinding
class IdentityDialog(
private val context: Context,
private val identity: Identities.Identity,
private val listener: Listener
) {
private lateinit var binding: DialogIdentityBinding
interface Listener {
fun onSaveIdentity(identity: Identities.Identity)
}
fun show() {
binding = DialogIdentityBinding.inflate(LayoutInflater.from(context))
binding.labelInput.setText(identity.name)
binding.aliasText.text = identity.key
AlertDialog.Builder(context)
.setTitle(R.string.edit_identity)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
identity.name = binding.labelInput.text.toString()
listener.onSaveIdentity(identity)
}
.create()
.show()
}
}

@ -0,0 +1,41 @@
package dev.lowrespalmtree.comet
import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import dev.lowrespalmtree.comet.Identities.Identity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.coroutineContext
class IdentityEditorFragment : PreferenceFragmentCompat() {
private val vm: IdentityEditorViewModel by viewModels()
private lateinit var namePref: EditTextPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.identity_preferences, rootKey)
namePref = findPreference<EditTextPreference>("name")!!
// vm.identity.observe(viewLifecycleOwner) {
// namePref.apply {
// // TODO
// }
// }
//
// arguments?.getLong("id")?.also { vm.loadIdentity(it) }
}
class IdentityEditorViewModel : ViewModel() {
val identity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() }
fun loadIdentity(id: Long) {
viewModelScope.launch(Dispatchers.IO) {
identity.postValue(Identities.get(id))
}
}
}
}

@ -17,7 +17,7 @@ import dev.lowrespalmtree.comet.databinding.*
* it could be a n-to-1 relation: many lines belong to the same block. This of course changes a bit
* the way we have to render things.
*/
class PageAdapter(private val listener: ContentAdapterListener) :
class PageAdapter(private val listener: Listener) :
RecyclerView.Adapter<PageAdapter.ContentViewHolder>() {
private var lines = listOf<Line>()
@ -25,7 +25,7 @@ class PageAdapter(private val listener: ContentAdapterListener) :
private var blocks = mutableListOf<ContentBlock>()
private var lastBlockCount = 0
interface ContentAdapterListener {
interface Listener {
fun onLinkClick(url: String)
}
@ -153,7 +153,7 @@ class PageAdapter(private val listener: ContentAdapterListener) :
}
}
is ContentBlock.Link -> {
val label = if (block.label.isNotBlank()) block.label else block.url
val label = block.label.ifBlank { block.url }
(holder as ContentViewHolder.Link).binding.textView.text = label
holder.binding.root.setOnClickListener { listener.onLinkClick(block.url) }
}

@ -22,12 +22,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lowrespalmtree.comet.PageAdapter.ContentAdapterListener
import dev.lowrespalmtree.comet.databinding.FragmentPageViewBinding
import dev.lowrespalmtree.comet.utils.isConnectedToNetwork
import dev.lowrespalmtree.comet.utils.joinUrls
import dev.lowrespalmtree.comet.utils.toGeminiUri
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ExperimentalCoroutinesApi
class PageFragment : Fragment(), ContentAdapterListener {
class PageFragment : Fragment(), PageAdapter.Listener {
private val vm: PageViewModel by viewModels()
private lateinit var binding: FragmentPageViewBinding
private lateinit var adapter: PageAdapter
@ -44,17 +46,17 @@ class PageFragment : Fragment(), ContentAdapterListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d(TAG, "onViewCreated")
adapter = PageAdapter(this)
binding.contentRecycler.layoutManager = LinearLayoutManager(requireContext())
adapter = PageAdapter(this)
binding.contentRecycler.adapter = adapter
binding.addressBar.setOnEditorActionListener { v, id, _ -> onAddressBarAction(v, id) }
binding.contentSwipeLayout.setOnRefreshListener { openUrl(vm.currentUrl) }
vm.state.observe(viewLifecycleOwner, { updateState(it) })
vm.lines.observe(viewLifecycleOwner, { updateLines(it) })
vm.event.observe(viewLifecycleOwner, { handleEvent(it) })
vm.state.observe(viewLifecycleOwner) { updateState(it) }
vm.lines.observe(viewLifecycleOwner) { updateLines(it) }
vm.event.observe(viewLifecycleOwner) { handleEvent(it) }
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
@ -71,7 +73,7 @@ class PageFragment : Fragment(), ContentAdapterListener {
}
override fun onLinkClick(url: String) {
openUrl(url, base = if (vm.currentUrl.isNotEmpty()) vm.currentUrl else null)
openUrl(url, base = vm.currentUrl.ifEmpty { null })
}
private fun onBackPressed() {
@ -208,7 +210,7 @@ class PageFragment : Fragment(), ContentAdapterListener {
})
}
AlertDialog.Builder(requireContext())
.setMessage(if (prompt.isNotEmpty()) prompt else "Input required")
.setMessage(prompt.ifEmpty { "Input required" })
.setView(inputView)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newUri = uri.buildUpon().query(editText.text.toString()).build()

@ -13,7 +13,6 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.nio.charset.Charset
@ExperimentalCoroutinesApi
class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedStateHandle) :
ViewModel() {
/** Currently viewed page URL. */
@ -52,6 +51,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
*
* The URI must be valid, absolute and with a gemini scheme.
*/
@ExperimentalCoroutinesApi
fun sendGeminiRequest(uri: Uri, connectionTimeout: Int, readTimeout: Int, redirects: Int = 0) {
Log.d(TAG, "sendRequest: URI \"$uri\"")
loadingUrl = uri
@ -110,6 +110,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
event.postValue(InputEvent(uri, response.meta))
}
@ExperimentalCoroutinesApi
private suspend fun handleSuccessResponse(response: Response, uri: Uri) {
state.postValue(State.RECEIVING)

@ -5,6 +5,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dev.lowrespalmtree.comet.utils.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@ -1,7 +0,0 @@
package dev.lowrespalmtree.comet
import android.content.Context
import android.widget.Toast
fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) =
Toast.makeText(context, stringId, length).show()

@ -1,4 +1,4 @@
package dev.lowrespalmtree.comet
package dev.lowrespalmtree.comet.utils
import android.content.Context
import android.net.ConnectivityManager

@ -0,0 +1,18 @@
package dev.lowrespalmtree.comet.utils
import android.content.Context
import android.util.TypedValue
import android.widget.Toast
import androidx.annotation.AttrRes
import dev.lowrespalmtree.comet.R
fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) =
Toast.makeText(context, stringId, length).show()
fun getDrawableFromAttr(context: Context, @AttrRes attr: Int) =
TypedValue()
.apply { context.theme.resolveAttribute(attr, this, true) }
.resourceId
fun getFancySelectBgRes(context: Context) =
getDrawableFromAttr(context, R.attr.selectableItemBackground)

@ -1,4 +1,4 @@
package dev.lowrespalmtree.comet
package dev.lowrespalmtree.comet.utils
import android.net.Uri

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/identity_name" />
<EditText
android:id="@+id/labelInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/identity_name"
android:importantForAutofill="no"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/identity_alias_title" />
<TextView
android:id="@+id/aliasText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true" />
</LinearLayout>

@ -1,43 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:layout_margin="4dp"
app:cardCornerRadius="4dp"
app:cardElevation="4dp"
android:foreground="?android:selectableItemBackground">
android:orientation="vertical">
<LinearLayout
<TextView
android:id="@+id/uriText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:textSize="14sp"
android:textColor="@color/text"
android:layout_margin="8dp"
android:fontFamily="@font/preformatted"
android:typeface="monospace" />
<TextView
android:id="@+id/uriText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text"
android:layout_margin="8dp"
android:fontFamily="@font/preformatted"
android:typeface="monospace" />
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceListItem" />
</androidx.cardview.widget.CardView>
</LinearLayout>

@ -1,21 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:layout_margin="4dp"
app:cardCornerRadius="4dp"
app:cardElevation="4dp"
android:foreground="?android:selectableItemBackground">
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
<TextView
android:id="@+id/labelText"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textSize="16sp"
android:textColor="@color/text" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/keyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="8dp"
android:textSize="12sp"
android:textColor="@color/text_light"
android:fontFamily="@font/preformatted"
android:typeface="monospace" />
</androidx.cardview.widget.CardView>
</LinearLayout>

@ -31,7 +31,16 @@
<fragment
android:id="@+id/identitiesFragment"
android:name="dev.lowrespalmtree.comet.IdentitiesFragment"
android:label="IdentitiesFragment" />
android:label="IdentitiesFragment" >
<action
android:id="@+id/action_identitiesFragment_to_identityEditorFragment"
app:destination="@id/identityEditorFragment" />
</fragment>
<fragment
android:id="@+id/identityEditorFragment"
android:name="dev.lowrespalmtree.comet.IdentityEditorFragment"
android:label="IdentityEditorFragment" />
<fragment
android:id="@+id/settingsFragment"
@ -44,4 +53,5 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</navigation>

@ -31,6 +31,14 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="identities">Identities</string>
<!-- Preference Titles -->
<string name="messages_header">Messages</string>
<string name="sync_header">Sync</string>
<string name="identity_key_setup">Setup key pair for this identity</string>
<string name="identity_name">Label</string>
<string name="generating_keypair">Generating client certificate…</string>
<string name="identity_alias_title">Keystore reference</string>
<string name="edit_identity">Edit identity</string>
</resources>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:persistent="false">
<EditTextPreference
android:key="name"
android:title="@string/identity_name"
app:useSimpleSummaryProvider="true" />
<Preference
android:key="key_setup"
android:title="@string/identity_key_setup" />
</androidx.preference.PreferenceScreen>

@ -1,5 +1,6 @@
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pref_general_header">
@ -25,20 +26,20 @@
<PreferenceCategory app:title="@string/pref_protocol_header">
<SeekBarPreference
android:defaultValue="10"
android:max="60"
app:key="connection_timeout"
app:title="@string/pref_connection_timeout_title"
app:seekBarIncrement="1"
app:showSeekBarValue="true"
android:max="60"
android:defaultValue="10" />
app:title="@string/pref_connection_timeout_title" />
<SeekBarPreference
android:defaultValue="10"
android:max="60"
app:key="read_timeout"
app:title="@string/pref_read_timeout_title"
app:seekBarIncrement="1"
app:showSeekBarValue="true"
android:max="60"
android:defaultValue="10" />
app:title="@string/pref_read_timeout_title" />
<SwitchPreferenceCompat
app:key="sync"

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
classpath 'com.android.tools.build:gradle:7.1.0'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
}

@ -1,6 +1,6 @@
#Mon Nov 29 12:01:36 CET 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

Loading…
Cancel
Save