Compare commits
2 commits
636a096d6f
...
e077a3f4c5
Author | SHA1 | Date | |
---|---|---|---|
dece | e077a3f4c5 | ||
dece | f45c3facfd |
|
@ -45,18 +45,18 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def nav_version = "2.3.5"
|
def nav_version = "2.4.0"
|
||||||
def room_version = "2.4.1"
|
def room_version = "2.4.1"
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
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.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.navigation:navigation-fragment-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||||
implementation "androidx.navigation:navigation-ui-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.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
implementation "androidx.room:room-ktx:$room_version"
|
implementation "androidx.room:room-ktx:$room_version"
|
||||||
|
|
|
@ -13,27 +13,36 @@ class UriUtilsTest {
|
||||||
fun joinUrls() {
|
fun joinUrls() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"gemini://dece.space/some-file.gmi",
|
"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(
|
assertEquals(
|
||||||
"gemini://dece.space/some-file.gmi",
|
"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(
|
assertEquals(
|
||||||
"gemini://dece.space/some-file.gmi",
|
"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(
|
assertEquals(
|
||||||
"gemini://dece.space/dir1/other-file.gmi",
|
"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(
|
assertEquals(
|
||||||
"gemini://dece.space/top-level.gmi",
|
"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(
|
assertEquals(
|
||||||
"s://hard/test/b/d/a.gmi",
|
"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("mid/content=5/../6", "mid/6"),
|
||||||
Pair("../../../../g", "g")
|
Pair("../../../../g", "g")
|
||||||
).forEach { (path, expected) ->
|
).forEach { (path, expected) ->
|
||||||
assertEquals(expected, removeDotSegments(path))
|
assertEquals(expected, dev.lowrespalmtree.comet.utils.removeDotSegments(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun removeLastSegment() {
|
fun removeLastSegment() {
|
||||||
assertEquals("", removeLastSegment(""))
|
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment(""))
|
||||||
assertEquals("", removeLastSegment("/"))
|
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment("/"))
|
||||||
assertEquals("", removeLastSegment("/a"))
|
assertEquals("", dev.lowrespalmtree.comet.utils.removeLastSegment("/a"))
|
||||||
assertEquals("/a", removeLastSegment("/a/"))
|
assertEquals("/a", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/"))
|
||||||
assertEquals("/a", removeLastSegment("/a/b"))
|
assertEquals("/a", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/b"))
|
||||||
assertEquals("/a/b/c", removeLastSegment("/a/b/c/d"))
|
assertEquals("/a/b/c", dev.lowrespalmtree.comet.utils.removeLastSegment("/a/b/c/d"))
|
||||||
assertEquals("//", removeLastSegment("///"))
|
assertEquals("//", dev.lowrespalmtree.comet.utils.removeLastSegment("///"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun popFirstSegment() {
|
fun popFirstSegment() {
|
||||||
assertEquals(Pair("", ""), popFirstSegment(""))
|
assertEquals(Pair("", ""), dev.lowrespalmtree.comet.utils.popFirstSegment(""))
|
||||||
assertEquals(Pair("a", ""), popFirstSegment("a"))
|
assertEquals(Pair("a", ""), dev.lowrespalmtree.comet.utils.popFirstSegment("a"))
|
||||||
assertEquals(Pair("/a", ""), popFirstSegment("/a"))
|
assertEquals(Pair("/a", ""), dev.lowrespalmtree.comet.utils.popFirstSegment("/a"))
|
||||||
assertEquals(Pair("/a", "/"), popFirstSegment("/a/"))
|
assertEquals(Pair("/a", "/"), dev.lowrespalmtree.comet.utils.popFirstSegment("/a/"))
|
||||||
assertEquals(Pair("/a", "/b"), popFirstSegment("/a/b"))
|
assertEquals(Pair("/a", "/b"), dev.lowrespalmtree.comet.utils.popFirstSegment("/a/b"))
|
||||||
assertEquals(Pair("a", "/b"), popFirstSegment("a/b"))
|
assertEquals(Pair("a", "/b"), dev.lowrespalmtree.comet.utils.popFirstSegment("a/b"))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package dev.lowrespalmtree.comet
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.AutoMigration
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
|
|
@ -7,12 +7,13 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.lowrespalmtree.comet.History.HistoryEntry
|
import dev.lowrespalmtree.comet.History.HistoryEntry
|
||||||
import dev.lowrespalmtree.comet.databinding.FragmentHistoryItemBinding
|
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>() {
|
RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
|
||||||
private var items = listOf<HistoryEntry>()
|
private var items = listOf<HistoryEntry>()
|
||||||
|
|
||||||
interface HistoryItemAdapterListener {
|
interface Listener {
|
||||||
fun onItemClick(url: String)
|
fun onItemClick(url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ class HistoryAdapter(private val listener: HistoryItemAdapterListener) :
|
||||||
holder.binding.titleText.visibility =
|
holder.binding.titleText.visibility =
|
||||||
if (item.title.isNullOrBlank()) View.GONE else View.VISIBLE
|
if (item.title.isNullOrBlank()) View.GONE else View.VISIBLE
|
||||||
holder.binding.titleText.text = item.title ?: ""
|
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
|
override fun getItemCount(): Int = items.size
|
||||||
|
@ -43,5 +44,9 @@ class HistoryAdapter(private val listener: HistoryItemAdapterListener) :
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(val binding: FragmentHistoryItemBinding) :
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dev.lowrespalmtree.comet.History.HistoryEntry
|
import dev.lowrespalmtree.comet.History.HistoryEntry
|
||||||
import dev.lowrespalmtree.comet.HistoryAdapter.HistoryItemAdapterListener
|
|
||||||
import dev.lowrespalmtree.comet.databinding.FragmentHistoryListBinding
|
import dev.lowrespalmtree.comet.databinding.FragmentHistoryListBinding
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
class HistoryFragment : Fragment(), HistoryAdapter.Listener {
|
||||||
class HistoryFragment : Fragment(), HistoryItemAdapterListener {
|
|
||||||
private val vm: HistoryViewModel by viewModels()
|
private val vm: HistoryViewModel by viewModels()
|
||||||
private lateinit var binding: FragmentHistoryListBinding
|
private lateinit var binding: FragmentHistoryListBinding
|
||||||
private lateinit var adapter: HistoryAdapter
|
private lateinit var adapter: HistoryAdapter
|
||||||
|
@ -36,11 +34,13 @@ class HistoryFragment : Fragment(), HistoryItemAdapterListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
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)
|
adapter = HistoryAdapter(this)
|
||||||
binding.list.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
binding.list.adapter = adapter
|
binding.list.adapter = adapter
|
||||||
|
|
||||||
vm.items.observe(viewLifecycleOwner, { adapter.setItems(it) })
|
vm.items.observe(viewLifecycleOwner) { adapter.setItems(it) }
|
||||||
|
|
||||||
vm.refreshHistory()
|
vm.refreshHistory()
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,6 @@ class HistoryFragment : Fragment(), HistoryItemAdapterListener {
|
||||||
findNavController().navigate(R.id.action_global_pageFragment, bundle)
|
findNavController().navigate(R.id.action_global_pageFragment, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
class HistoryViewModel(
|
class HistoryViewModel(
|
||||||
@Suppress("unused") private val savedStateHandle: SavedStateHandle
|
@Suppress("unused") private val savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package dev.lowrespalmtree.comet
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
|
||||||
object Identities {
|
object Identities {
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -10,7 +14,7 @@ object Identities {
|
||||||
/** Key to retrieve certificate from the keystore. */
|
/** Key to retrieve certificate from the keystore. */
|
||||||
val key: String,
|
val key: String,
|
||||||
/** Label for this identity. */
|
/** Label for this identity. */
|
||||||
val name: String?,
|
var name: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -26,13 +30,44 @@ object Identities {
|
||||||
@Dao
|
@Dao
|
||||||
interface IdentityDao {
|
interface IdentityDao {
|
||||||
@Insert
|
@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")
|
@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))
|
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
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.lowrespalmtree.comet.Identities.Identity
|
import dev.lowrespalmtree.comet.Identities.Identity
|
||||||
import dev.lowrespalmtree.comet.databinding.FragmentIdentityBinding
|
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>()
|
private var identities = listOf<Identity>()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryAdapter.ViewHolder {
|
interface Listener {
|
||||||
TODO("Not yet implemented")
|
fun onIdentityClick(identity: Identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: HistoryAdapter.ViewHolder, position: Int) {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||||
TODO("Not yet implemented")
|
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 {
|
override fun getItemCount(): Int = identities.size
|
||||||
TODO("Not yet implemented")
|
|
||||||
|
@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
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import 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.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 binding: FragmentIdentitiesBinding
|
||||||
|
private lateinit var adapter: IdentitiesAdapter
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -18,4 +31,59 @@ class IdentitiesFragment : Fragment() {
|
||||||
binding = FragmentIdentitiesBinding.inflate(layoutInflater)
|
binding = FragmentIdentitiesBinding.inflate(layoutInflater)
|
||||||
return binding.root
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
33
app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt
Normal file
33
app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt
Normal file
|
@ -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
|
* 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.
|
* the way we have to render things.
|
||||||
*/
|
*/
|
||||||
class PageAdapter(private val listener: ContentAdapterListener) :
|
class PageAdapter(private val listener: Listener) :
|
||||||
RecyclerView.Adapter<PageAdapter.ContentViewHolder>() {
|
RecyclerView.Adapter<PageAdapter.ContentViewHolder>() {
|
||||||
|
|
||||||
private var lines = listOf<Line>()
|
private var lines = listOf<Line>()
|
||||||
|
@ -25,7 +25,7 @@ class PageAdapter(private val listener: ContentAdapterListener) :
|
||||||
private var blocks = mutableListOf<ContentBlock>()
|
private var blocks = mutableListOf<ContentBlock>()
|
||||||
private var lastBlockCount = 0
|
private var lastBlockCount = 0
|
||||||
|
|
||||||
interface ContentAdapterListener {
|
interface Listener {
|
||||||
fun onLinkClick(url: String)
|
fun onLinkClick(url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ class PageAdapter(private val listener: ContentAdapterListener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ContentBlock.Link -> {
|
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 as ContentViewHolder.Link).binding.textView.text = label
|
||||||
holder.binding.root.setOnClickListener { listener.onLinkClick(block.url) }
|
holder.binding.root.setOnClickListener { listener.onLinkClick(block.url) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,14 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dev.lowrespalmtree.comet.PageAdapter.ContentAdapterListener
|
|
||||||
import dev.lowrespalmtree.comet.databinding.FragmentPageViewBinding
|
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
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class PageFragment : Fragment(), ContentAdapterListener {
|
class PageFragment : Fragment(), PageAdapter.Listener {
|
||||||
private val vm: PageViewModel by viewModels()
|
private val vm: PageViewModel by viewModels()
|
||||||
private lateinit var binding: FragmentPageViewBinding
|
private lateinit var binding: FragmentPageViewBinding
|
||||||
private lateinit var adapter: PageAdapter
|
private lateinit var adapter: PageAdapter
|
||||||
|
@ -44,17 +46,17 @@ class PageFragment : Fragment(), ContentAdapterListener {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
Log.d(TAG, "onViewCreated")
|
Log.d(TAG, "onViewCreated")
|
||||||
adapter = PageAdapter(this)
|
|
||||||
binding.contentRecycler.layoutManager = LinearLayoutManager(requireContext())
|
binding.contentRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = PageAdapter(this)
|
||||||
binding.contentRecycler.adapter = adapter
|
binding.contentRecycler.adapter = adapter
|
||||||
|
|
||||||
binding.addressBar.setOnEditorActionListener { v, id, _ -> onAddressBarAction(v, id) }
|
binding.addressBar.setOnEditorActionListener { v, id, _ -> onAddressBarAction(v, id) }
|
||||||
|
|
||||||
binding.contentSwipeLayout.setOnRefreshListener { openUrl(vm.currentUrl) }
|
binding.contentSwipeLayout.setOnRefreshListener { openUrl(vm.currentUrl) }
|
||||||
|
|
||||||
vm.state.observe(viewLifecycleOwner, { updateState(it) })
|
vm.state.observe(viewLifecycleOwner) { updateState(it) }
|
||||||
vm.lines.observe(viewLifecycleOwner, { updateLines(it) })
|
vm.lines.observe(viewLifecycleOwner) { updateLines(it) }
|
||||||
vm.event.observe(viewLifecycleOwner, { handleEvent(it) })
|
vm.event.observe(viewLifecycleOwner) { handleEvent(it) }
|
||||||
|
|
||||||
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
|
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
|
||||||
|
|
||||||
|
@ -71,7 +73,7 @@ class PageFragment : Fragment(), ContentAdapterListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLinkClick(url: String) {
|
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() {
|
private fun onBackPressed() {
|
||||||
|
@ -208,7 +210,7 @@ class PageFragment : Fragment(), ContentAdapterListener {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(if (prompt.isNotEmpty()) prompt else "Input required")
|
.setMessage(prompt.ifEmpty { "Input required" })
|
||||||
.setView(inputView)
|
.setView(inputView)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
val newUri = uri.buildUpon().query(editText.text.toString()).build()
|
val newUri = uri.buildUpon().query(editText.text.toString()).build()
|
||||||
|
|
|
@ -13,7 +13,6 @@ import java.net.SocketTimeoutException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedStateHandle) :
|
class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedStateHandle) :
|
||||||
ViewModel() {
|
ViewModel() {
|
||||||
/** Currently viewed page URL. */
|
/** 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.
|
* The URI must be valid, absolute and with a gemini scheme.
|
||||||
*/
|
*/
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
fun sendGeminiRequest(uri: Uri, connectionTimeout: Int, readTimeout: Int, redirects: Int = 0) {
|
fun sendGeminiRequest(uri: Uri, connectionTimeout: Int, readTimeout: Int, redirects: Int = 0) {
|
||||||
Log.d(TAG, "sendRequest: URI \"$uri\"")
|
Log.d(TAG, "sendRequest: URI \"$uri\"")
|
||||||
loadingUrl = uri
|
loadingUrl = uri
|
||||||
|
@ -110,6 +110,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
|
||||||
event.postValue(InputEvent(uri, response.meta))
|
event.postValue(InputEvent(uri, response.meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
private suspend fun handleSuccessResponse(response: Response, uri: Uri) {
|
private suspend fun handleSuccessResponse(response: Response, uri: Uri) {
|
||||||
state.postValue(State.RECEIVING)
|
state.postValue(State.RECEIVING)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import dev.lowrespalmtree.comet.utils.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
18
app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt
Normal file
18
app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt
Normal file
|
@ -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
|
import android.net.Uri
|
||||||
|
|
32
app/src/main/res/layout/dialog_identity.xml
Normal file
32
app/src/main/res/layout/dialog_identity.xml
Normal file
|
@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.cardview.widget.CardView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
android:id="@+id/container"
|
||||||
android:id="@+id/card"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:layout_margin="4dp"
|
android:orientation="vertical">
|
||||||
app:cardCornerRadius="4dp"
|
|
||||||
app:cardElevation="4dp"
|
|
||||||
android:foreground="?android:selectableItemBackground">
|
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
|
android:id="@+id/uriText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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
|
<TextView
|
||||||
android:id="@+id/uriText"
|
android:id="@+id/titleText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@color/text"
|
android:textColor="@color/text"
|
||||||
android:layout_margin="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:fontFamily="@font/preformatted"
|
android:layout_marginEnd="8dp"
|
||||||
android:typeface="monospace" />
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
|
||||||
<TextView
|
</LinearLayout>
|
||||||
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>
|
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
|
@ -1,21 +1,32 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.cardview.widget.CardView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
android:id="@+id/container"
|
||||||
android:id="@+id/card"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:layout_margin="4dp"
|
android:orientation="vertical">
|
||||||
app:cardCornerRadius="4dp"
|
|
||||||
app:cardElevation="4dp"
|
|
||||||
android:foreground="?android:selectableItemBackground">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<TextView
|
||||||
|
android:id="@+id/labelText"
|
||||||
android:layout_width="match_parent"
|
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
|
<fragment
|
||||||
android:id="@+id/identitiesFragment"
|
android:id="@+id/identitiesFragment"
|
||||||
android:name="dev.lowrespalmtree.comet.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
|
<fragment
|
||||||
android:id="@+id/settingsFragment"
|
android:id="@+id/settingsFragment"
|
||||||
|
@ -44,4 +53,5 @@
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
|
@ -31,6 +31,14 @@
|
||||||
<!-- TODO: Remove or change this placeholder text -->
|
<!-- TODO: Remove or change this placeholder text -->
|
||||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||||
<string name="identities">Identities</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>
|
</resources>
|
16
app/src/main/res/xml/identity_preferences.xml
Normal file
16
app/src/main/res/xml/identity_preferences.xml
Normal file
|
@ -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"
|
<androidx.preference.PreferenceScreen
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
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">
|
<PreferenceCategory app:title="@string/pref_general_header">
|
||||||
|
|
||||||
|
@ -25,20 +26,20 @@
|
||||||
<PreferenceCategory app:title="@string/pref_protocol_header">
|
<PreferenceCategory app:title="@string/pref_protocol_header">
|
||||||
|
|
||||||
<SeekBarPreference
|
<SeekBarPreference
|
||||||
|
android:defaultValue="10"
|
||||||
|
android:max="60"
|
||||||
app:key="connection_timeout"
|
app:key="connection_timeout"
|
||||||
app:title="@string/pref_connection_timeout_title"
|
|
||||||
app:seekBarIncrement="1"
|
app:seekBarIncrement="1"
|
||||||
app:showSeekBarValue="true"
|
app:showSeekBarValue="true"
|
||||||
android:max="60"
|
app:title="@string/pref_connection_timeout_title" />
|
||||||
android:defaultValue="10" />
|
|
||||||
|
|
||||||
<SeekBarPreference
|
<SeekBarPreference
|
||||||
|
android:defaultValue="10"
|
||||||
|
android:max="60"
|
||||||
app:key="read_timeout"
|
app:key="read_timeout"
|
||||||
app:title="@string/pref_read_timeout_title"
|
|
||||||
app:seekBarIncrement="1"
|
app:seekBarIncrement="1"
|
||||||
app:showSeekBarValue="true"
|
app:showSeekBarValue="true"
|
||||||
android:max="60"
|
app:title="@string/pref_read_timeout_title" />
|
||||||
android:defaultValue="10" />
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="sync"
|
app:key="sync"
|
||||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
|
||||||
}
|
}
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Mon Nov 29 12:01:36 CET 2021
|
#Mon Nov 29 12:01:36 CET 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
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
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
Reference in a new issue