Compare commits
2 commits
636a096d6f
...
e077a3f4c5
Author | SHA1 | Date | |
---|---|---|---|
dece | e077a3f4c5 | ||
dece | f45c3facfd |
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
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
|
||||
* 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
|
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
|
||||
|
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,27 +1,18 @@
|
|||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uriText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text"
|
||||
android:layout_margin="8dp"
|
||||
android:fontFamily="@font/preformatted"
|
||||
|
@ -31,13 +22,12 @@
|
|||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
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" />
|
||||
</LinearLayout>
|
||||
|
||||
</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>
|
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"
|
||||
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"
|
||||
}
|
||||
|
|
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
|
||||
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
|
||||
|
|
Reference in a new issue