Compare commits

..

3 commits

Author SHA1 Message Date
dece 020f48149b PageViewModel: docs 2022-01-25 19:36:59 +01:00
dece f02220f430 PageFragment: move some data to the view model
This should advance on #4.
2022-01-25 19:32:05 +01:00
dece 4ff8e82271 cleanup and renaming things around 2022-01-25 18:55:46 +01:00
7 changed files with 101 additions and 83 deletions

View file

@ -56,7 +56,7 @@ dependencies {
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.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0' 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"
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"

View file

@ -47,7 +47,7 @@ class HistoryFragment : Fragment(), HistoryItemAdapterListener {
override fun onItemClick(url: String) { override fun onItemClick(url: String) {
val bundle = bundleOf("url" to url) val bundle = bundleOf("url" to url)
findNavController().navigate(R.id.action_global_pageViewFragment, bundle) findNavController().navigate(R.id.action_global_pageFragment, bundle)
} }
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi

View file

@ -6,7 +6,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import dev.lowrespalmtree.comet.databinding.ActivityMainBinding import dev.lowrespalmtree.comet.databinding.ActivityMainBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -27,10 +26,10 @@ class MainActivity : AppCompatActivity() {
} }
/** Navigate to the PageViewFragment; this will automatically use the home URL if any. */ /** Navigate to the PageViewFragment; this will automatically use the home URL if any. */
fun goHome(item: MenuItem) { fun goHome(@Suppress("unused_parameter") item: MenuItem) {
val bundle = bundleOf() val bundle = bundleOf()
Preferences.getHomeUrl(this)?.let { bundle.putString("url", it) } Preferences.getHomeUrl(this)?.let { bundle.putString("url", it) }
nhf?.navController?.navigate(R.id.action_global_pageViewFragment, bundle) nhf?.navController?.navigate(R.id.action_global_pageFragment, bundle)
binding.drawerLayout.closeDrawers() binding.drawerLayout.closeDrawers()
} }
} }

View file

@ -19,8 +19,8 @@ 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 ContentAdapter(private val listener: ContentAdapterListener) : class PageAdapter(private val listener: ContentAdapterListener) :
RecyclerView.Adapter<ContentAdapter.ContentViewHolder>() { RecyclerView.Adapter<PageAdapter.ContentViewHolder>() {
private var lines = listOf<Line>() private var lines = listOf<Line>()
private var currentLine = 0 private var currentLine = 0
@ -161,7 +161,7 @@ class ContentAdapter(private val listener: ContentAdapterListener) :
override fun getItemCount(): Int = blocks.size override fun getItemCount(): Int = blocks.size
companion object { companion object {
private const val TAG = "ContentRecycler" private const val TAG = "PageAdapter"
private const val TYPE_EMPTY = 0 private const val TYPE_EMPTY = 0
private const val TYPE_TITLE_1 = 1 private const val TYPE_TITLE_1 = 1
private const val TYPE_TITLE_2 = 2 private const val TYPE_TITLE_2 = 2

View file

@ -19,39 +19,40 @@ import androidx.activity.addCallback
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider 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.ContentAdapter.ContentAdapterListener import dev.lowrespalmtree.comet.PageAdapter.ContentAdapterListener
import dev.lowrespalmtree.comet.databinding.FragmentPageViewBinding import dev.lowrespalmtree.comet.databinding.FragmentPageViewBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class PageViewFragment : Fragment(), ContentAdapterListener { class PageFragment : Fragment(), ContentAdapterListener {
private val vm: PageViewModel by viewModels()
private lateinit var binding: FragmentPageViewBinding private lateinit var binding: FragmentPageViewBinding
private lateinit var pageViewModel: PageViewModel private lateinit var adapter: PageAdapter
private lateinit var adapter: ContentAdapter
/** Property to access and set the current address bar URL value. */ /** Property to access and set the current address bar URL value. */
private var currentUrl private var currentUrl: String
get() = binding.addressBar.text.toString() get() = vm.currentUrl
set(value) = binding.addressBar.setText(value) set(value) {
vm.currentUrl = value
/** A non-saved list of visited URLs. Not an history, just used for going back. */ binding.addressBar.setText(value)
private val visitedUrls = mutableListOf<String>() }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
Log.d(TAG, "onCreateView")
binding = FragmentPageViewBinding.inflate(layoutInflater) binding = FragmentPageViewBinding.inflate(layoutInflater)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
pageViewModel = ViewModelProvider(this)[PageViewModel::class.java] Log.d(TAG, "onViewCreated")
adapter = ContentAdapter(this) adapter = PageAdapter(this)
binding.contentRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.contentRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.contentRecycler.adapter = adapter binding.contentRecycler.adapter = adapter
@ -59,16 +60,20 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
binding.contentSwipeLayout.setOnRefreshListener { openUrl(currentUrl) } binding.contentSwipeLayout.setOnRefreshListener { openUrl(currentUrl) }
pageViewModel.state.observe(viewLifecycleOwner, { updateState(it) }) vm.state.observe(viewLifecycleOwner, { updateState(it) })
pageViewModel.lines.observe(viewLifecycleOwner, { updateLines(it) }) vm.lines.observe(viewLifecycleOwner, { updateLines(it) })
pageViewModel.event.observe(viewLifecycleOwner, { handleEvent(it) }) vm.event.observe(viewLifecycleOwner, { handleEvent(it) })
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() } activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
val url = arguments?.getString("url") val url = arguments?.getString("url")
if (!url.isNullOrEmpty()) { if (!url.isNullOrEmpty()) {
Log.d(TAG, "onViewCreated: open \"$url\"")
openUrl(url) openUrl(url)
} else if (visitedUrls.isEmpty()) { } else if (vm.currentUrl.isNotEmpty()) {
Log.d(TAG, "onViewCreated: reuse current URL, probably fragment recreation")
} else if (vm.visitedUrls.isEmpty()) {
Log.d(TAG, "onViewCreated: no current URL, open home if configured")
Preferences.getHomeUrl(requireContext())?.let { openUrl(it) } Preferences.getHomeUrl(requireContext())?.let { openUrl(it) }
} }
} }
@ -78,9 +83,9 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
} }
private fun onBackPressed() { private fun onBackPressed() {
if (visitedUrls.size >= 2) { if (vm.visitedUrls.size >= 2) {
visitedUrls.removeLastOrNull() // Always remove current page first. vm.visitedUrls.removeLastOrNull() // Always remove current page first.
visitedUrls.removeLastOrNull()?.also { openUrl(it) } vm.visitedUrls.removeLastOrNull()?.also { openUrl(it) }
} }
} }
@ -127,7 +132,7 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
prefs.getInt("connection_timeout", Request.DEFAULT_CONNECTION_TIMEOUT_SEC) prefs.getInt("connection_timeout", Request.DEFAULT_CONNECTION_TIMEOUT_SEC)
val readTimeout = val readTimeout =
prefs.getInt("read_timeout", Request.DEFAULT_READ_TIMEOUT_SEC) prefs.getInt("read_timeout", Request.DEFAULT_READ_TIMEOUT_SEC)
pageViewModel.sendGeminiRequest(uri, connectionTimeout, readTimeout) vm.sendGeminiRequest(uri, connectionTimeout, readTimeout)
} }
else -> openUnknownScheme(uri) else -> openUnknownScheme(uri)
} }
@ -157,53 +162,30 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
private fun handleEvent(event: PageViewModel.Event) { private fun handleEvent(event: PageViewModel.Event) {
Log.d(TAG, "handleEvent: $event") Log.d(TAG, "handleEvent: $event")
if (!event.handled) { if (event.handled)
when (event) { return
is PageViewModel.InputEvent -> { when (event) {
val editText = EditText(requireContext()) is PageViewModel.InputEvent -> {
editText.inputType = InputType.TYPE_CLASS_TEXT askForInput(event.prompt, event.uri)
val inputView = FrameLayout(requireContext()).apply { }
addView(FrameLayout(requireContext()).apply { is PageViewModel.SuccessEvent -> {
addView(editText) currentUrl = event.uri
val params = FrameLayout.LayoutParams( vm.visitedUrls.add(event.uri)
FrameLayout.LayoutParams.MATCH_PARENT, }
FrameLayout.LayoutParams.WRAP_CONTENT is PageViewModel.RedirectEvent -> {
) openUrl(event.uri, base = currentUrl, redirections = event.redirects)
params.setMargins(resources.getDimensionPixelSize(R.dimen.text_margin)) }
layoutParams = params is PageViewModel.FailureEvent -> {
}) var message = event.details
} if (!event.serverDetails.isNullOrEmpty())
AlertDialog.Builder(requireContext()) message += "\n\nServer details: ${event.serverDetails}"
.setMessage(if (event.prompt.isNotEmpty()) event.prompt else "Input required") if (!isConnectedToNetwork(requireContext()))
.setView(inputView) message += "\n\nInternet may be inaccessible…"
.setPositiveButton(android.R.string.ok) { _, _ -> alert(message, title = event.short)
val newUri = updateState(PageViewModel.State.IDLE)
event.uri.buildUpon().query(editText.text.toString()).build()
openUrl(newUri.toString(), base = currentUrl)
}
.setOnDismissListener { updateState(PageViewModel.State.IDLE) }
.create()
.show()
}
is PageViewModel.SuccessEvent -> {
currentUrl = event.uri
visitedUrls.add(event.uri)
}
is PageViewModel.RedirectEvent -> {
openUrl(event.uri, base = currentUrl, redirections = event.redirects)
}
is PageViewModel.FailureEvent -> {
var message = event.details
if (!event.serverDetails.isNullOrEmpty())
message += "\n\nServer details: ${event.serverDetails}"
if (!isConnectedToNetwork(requireContext()))
message += "\n\nInternet may be inaccessible…"
alert(message, title = event.short)
updateState(PageViewModel.State.IDLE)
}
} }
event.handled = true
} }
event.handled = true
} }
private fun alert(message: String, title: String? = null) { private fun alert(message: String, title: String? = null) {
@ -214,6 +196,32 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
.show() .show()
} }
private fun askForInput(prompt: String, uri: Uri) {
val editText = EditText(requireContext())
editText.inputType = InputType.TYPE_CLASS_TEXT
val inputView = FrameLayout(requireContext()).apply {
addView(FrameLayout(requireContext()).apply {
addView(editText)
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(resources.getDimensionPixelSize(R.dimen.text_margin))
layoutParams = params
})
}
AlertDialog.Builder(requireContext())
.setMessage(if (prompt.isNotEmpty()) prompt else "Input required")
.setView(inputView)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newUri = uri.buildUpon().query(editText.text.toString()).build()
openUrl(newUri.toString(), base = currentUrl)
}
.setOnDismissListener { updateState(PageViewModel.State.IDLE) }
.create()
.show()
}
private fun openUnknownScheme(uri: Uri) { private fun openUnknownScheme(uri: Uri) {
try { try {
startActivity(Intent(Intent.ACTION_VIEW, uri)) startActivity(Intent(Intent.ACTION_VIEW, uri))
@ -223,6 +231,6 @@ class PageViewFragment : Fragment(), ContentAdapterListener {
} }
companion object { companion object {
private const val TAG = "PageViewFragment" private const val TAG = "PageFragment"
} }
} }

View file

@ -3,6 +3,7 @@ package dev.lowrespalmtree.comet
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -13,12 +14,22 @@ import java.net.UnknownHostException
import java.nio.charset.Charset import java.nio.charset.Charset
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class PageViewModel : ViewModel() { class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedStateHandle) :
private var requestJob: Job? = null ViewModel() {
/** Currently viewed page URL. */
var currentUrl: String = ""
/** Observable page viewer state. */
val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) } val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) }
private var linesList = ArrayList<Line>() /** Observable page viewer lines (backed up by `linesList` but updated less often). */
val lines: MutableLiveData<List<Line>> by lazy { MutableLiveData<List<Line>>() } val lines: MutableLiveData<List<Line>> by lazy { MutableLiveData<List<Line>>() }
/** Observable page viewer latest event. */
val event: MutableLiveData<Event> by lazy { MutableLiveData<Event>() } val event: MutableLiveData<Event> by lazy { MutableLiveData<Event>() }
/** A non-saved list of visited URLs. Not an history, just used for going back. */
val visitedUrls = mutableListOf<String>()
/** Latest request job created, stored to cancel it if needed. */
private var requestJob: Job? = null
/** Lines for the current page. */
private var linesList = ArrayList<Line>()
enum class State { enum class State {
IDLE, CONNECTING, RECEIVING IDLE, CONNECTING, RECEIVING

View file

@ -2,15 +2,15 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main" android:id="@+id/main"
app:startDestination="@id/pageViewFragment"> app:startDestination="@id/pageFragment">
<fragment <fragment
android:id="@+id/pageViewFragment" android:id="@+id/pageFragment"
android:name="dev.lowrespalmtree.comet.PageViewFragment" android:name="dev.lowrespalmtree.comet.PageFragment"
android:label="PageViewFragment" /> android:label="PageViewFragment" />
<action <action
android:id="@+id/action_global_pageViewFragment" android:id="@+id/action_global_pageFragment"
app:destination="@id/pageViewFragment" app:destination="@id/pageFragment"
app:enterAnim="@anim/nav_default_enter_anim" app:enterAnim="@anim/nav_default_enter_anim"
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"