PageAdapter: show visited links with another color

This commit is contained in:
dece 2022-02-13 18:13:32 +01:00
parent a822287de2
commit 8c1bebf315
8 changed files with 48 additions and 15 deletions

View file

@ -13,7 +13,7 @@ interface Line
class EmptyLine : Line class EmptyLine : Line
class ParagraphLine(val text: String) : Line class ParagraphLine(val text: String) : Line
class TitleLine(val level: Int, val text: String) : Line class TitleLine(val level: Int, val text: String) : Line
class LinkLine(val url: String, val label: String) : Line class LinkLine(val url: String, val label: String, var visited: Boolean = false) : Line
class PreFenceLine(val caption: String) : Line class PreFenceLine(val caption: String) : Line
class PreTextLine(val text: String) : Line class PreTextLine(val text: String) : Line
class BlockquoteLine(val text: String) : Line class BlockquoteLine(val text: String) : Line
@ -21,7 +21,7 @@ class ListItemLine(val text: String) : Line
private const val TAG = "Gemtext" private const val TAG = "Gemtext"
/** Pipe incoming gemtext data into parsed Lines. */ /** Pipe incoming Gemtext data into parsed Lines. */
fun parseData( fun parseData(
inChannel: Channel<ByteArray>, inChannel: Channel<ByteArray>,
charset: Charset, charset: Charset,

View file

@ -38,6 +38,10 @@ object History {
dao.update(entry.also { it.title = title; it.lastVisit = now }) dao.update(entry.also { it.title = title; it.lastVisit = now })
} }
suspend fun contains(uri: String): Boolean = get(uri) != null
suspend fun get(uri: String): HistoryEntry? = Database.INSTANCE.historyEntryDao().get(uri)
suspend fun getAll(): List<HistoryEntry> = Database.INSTANCE.historyEntryDao().getAll() suspend fun getAll(): List<HistoryEntry> = Database.INSTANCE.historyEntryDao().getAll()
suspend fun getLast(): HistoryEntry? = Database.INSTANCE.historyEntryDao().getLast() suspend fun getLast(): HistoryEntry? = Database.INSTANCE.historyEntryDao().getLast()

View file

@ -1,6 +1,7 @@
package dev.lowrespalmtree.comet package dev.lowrespalmtree.comet
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@ -15,6 +16,7 @@ class MainActivity : AppCompatActivity() {
private var nhf: NavHostFragment? = null private var nhf: NavHostFragment? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -32,4 +34,8 @@ class MainActivity : AppCompatActivity() {
nhf?.navController?.navigate(R.id.action_global_pageFragment, bundle) nhf?.navController?.navigate(R.id.action_global_pageFragment, bundle)
binding.drawerLayout.closeDrawers() binding.drawerLayout.closeDrawers()
} }
companion object {
private const val TAG = "MainActivity"
}
} }

View file

@ -1,6 +1,7 @@
package dev.lowrespalmtree.comet package dev.lowrespalmtree.comet
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.Uri
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -33,7 +34,7 @@ class PageAdapter(private val listener: Listener) :
object Empty : ContentBlock() object Empty : ContentBlock()
class Paragraph(val text: String) : ContentBlock() class Paragraph(val text: String) : ContentBlock()
class Title(val text: String, val level: Int) : ContentBlock() class Title(val text: String, val level: Int) : ContentBlock()
class Link(val url: String, val label: String) : ContentBlock() class Link(val url: String, val label: String, val visited: Boolean) : ContentBlock()
class Pre(val caption: String, var content: String, var closed: Boolean) : ContentBlock() class Pre(val caption: String, var content: String, var closed: Boolean) : ContentBlock()
class Blockquote(var text: String) : ContentBlock() class Blockquote(var text: String) : ContentBlock()
class ListItem(val text: String) : ContentBlock() class ListItem(val text: String) : ContentBlock()
@ -54,7 +55,7 @@ class PageAdapter(private val listener: Listener) :
when (val line = lines[currentLine]) { when (val line = lines[currentLine]) {
is EmptyLine -> blocks.add(ContentBlock.Empty) is EmptyLine -> blocks.add(ContentBlock.Empty)
is ParagraphLine -> blocks.add(ContentBlock.Paragraph(line.text)) is ParagraphLine -> blocks.add(ContentBlock.Paragraph(line.text))
is LinkLine -> blocks.add(ContentBlock.Link(line.url, line.label)) is LinkLine -> blocks.add(ContentBlock.Link(line.url, line.label, line.visited))
is ListItemLine -> blocks.add(ContentBlock.ListItem(line.text)) is ListItemLine -> blocks.add(ContentBlock.ListItem(line.text))
is TitleLine -> blocks.add(ContentBlock.Title(line.text, line.level)) is TitleLine -> blocks.add(ContentBlock.Title(line.text, line.level))
is PreFenceLine -> { is PreFenceLine -> {
@ -95,8 +96,8 @@ class PageAdapter(private val listener: Listener) :
lastBlockCount = blocks.size lastBlockCount = blocks.size
} }
sealed class ContentViewHolder(view: View) : RecyclerView.ViewHolder(view) { sealed class ContentViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
class Empty(binding: GemtextEmptyBinding) : ContentViewHolder(binding.root) class Empty(val binding: GemtextEmptyBinding) : ContentViewHolder(binding.root)
class Paragraph(val binding: GemtextParagraphBinding) : ContentViewHolder(binding.root) class Paragraph(val binding: GemtextParagraphBinding) : ContentViewHolder(binding.root)
class Title1(val binding: GemtextTitle1Binding) : ContentViewHolder(binding.root) class Title1(val binding: GemtextTitle1Binding) : ContentViewHolder(binding.root)
class Title2(val binding: GemtextTitle2Binding) : ContentViewHolder(binding.root) class Title2(val binding: GemtextTitle2Binding) : ContentViewHolder(binding.root)
@ -156,6 +157,15 @@ class PageAdapter(private val listener: Listener) :
val label = block.label.ifBlank { 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) }
// Color links differently if it has been already visited or not.
val resources = holder.binding.root.context.resources
holder.binding.textView.setTextColor(
if (block.visited)
resources.getColor(R.color.link_visited, null)
else
resources.getColor(R.color.link, null)
)
} }
is ContentBlock.Pre -> is ContentBlock.Pre ->
(holder as ContentViewHolder.Pre).binding.textView.text = block.content (holder as ContentViewHolder.Pre).binding.textView.text = block.content

View file

@ -51,7 +51,7 @@ class PageFragment : Fragment(), PageAdapter.Listener {
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.second, it.first) }
vm.event.observe(viewLifecycleOwner) { handleEvent(it) } vm.event.observe(viewLifecycleOwner) { handleEvent(it) }
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() } activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
@ -151,8 +151,8 @@ class PageFragment : Fragment(), PageAdapter.Listener {
} }
} }
private fun updateLines(lines: List<Line>) { private fun updateLines(lines: List<Line>, url: String) {
Log.d(TAG, "updateLines: ${lines.size} lines") Log.d(TAG, "updateLines: ${lines.size} lines from $url")
adapter.setLines(lines) adapter.setLines(lines)
} }

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dev.lowrespalmtree.comet.utils.joinUrls
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.onSuccess import kotlinx.coroutines.channels.onSuccess
import java.net.ConnectException import java.net.ConnectException
@ -21,8 +22,8 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
var loadingUrl: Uri? = null var loadingUrl: Uri? = null
/** Observable page viewer state. */ /** Observable page viewer state. */
val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) } val state: MutableLiveData<State> by lazy { MutableLiveData<State>(State.IDLE) }
/** Observable page viewer lines (backed up by `linesList` but updated less often). */ /** Observable page viewer lines (backed up by `linesList` but updated less often). Left element is associated URL. */
val lines: MutableLiveData<List<Line>> by lazy { MutableLiveData<List<Line>>() } val lines: MutableLiveData<Pair<String, List<Line>>> by lazy { MutableLiveData<Pair<String, List<Line>>>() }
/** Observable page viewer latest event. */ /** 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. */ /** A non-saved list of visited URLs. Not an history, just used for going back. */
@ -113,9 +114,10 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
@ExperimentalCoroutinesApi @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)
val uriString = uri.toString()
linesList.clear() linesList.clear()
lines.postValue(linesList) lines.postValue(Pair(uriString, linesList))
val charset = Charset.defaultCharset() val charset = Charset.defaultCharset()
var mainTitle: String? = null var mainTitle: String? = null
var lastUpdate = System.currentTimeMillis() var lastUpdate = System.currentTimeMillis()
@ -126,7 +128,16 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
while (!lineChannel.isClosedForReceive) { while (!lineChannel.isClosedForReceive) {
val lineChannelResult = withTimeout(100) { lineChannel.tryReceive() } val lineChannelResult = withTimeout(100) { lineChannel.tryReceive() }
lineChannelResult.onSuccess { line -> lineChannelResult.onSuccess { line ->
if (line is LinkLine) {
// Mark visited links here as we have a access to the history.
val fullUrl =
if (Uri.parse(line.url).isAbsolute) line.url
else joinUrls(uriString, line.url).toString()
if (History.contains(fullUrl))
line.visited = true
}
linesList.add(line) linesList.add(line)
// Get the first level 1 header as the page main title.
if (mainTitle == null && line is TitleLine && line.level == 1) if (mainTitle == null && line is TitleLine && line.level == 1)
mainTitle = line.text mainTitle = line.text
} }
@ -135,7 +146,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
if (linesList.size > lastNumLines) { if (linesList.size > lastNumLines) {
val time = System.currentTimeMillis() val time = System.currentTimeMillis()
if (time - lastUpdate >= 100) { if (time - lastUpdate >= 100) {
lines.postValue(linesList) lines.postValue(Pair(uriString, linesList))
lastUpdate = time lastUpdate = time
lastNumLines = linesList.size lastNumLines = linesList.size
} }
@ -147,7 +158,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
return return
} }
Log.d(TAG, "handleSuccessResponse: done parsing line data") Log.d(TAG, "handleSuccessResponse: done parsing line data")
lines.postValue(linesList) lines.postValue(Pair(uriString, linesList))
// We record the history entry here: it's nice because we have the main title available // We record the history entry here: it's nice because we have the main title available
// and we're already in a coroutine for database access. // and we're already in a coroutine for database access.

View file

@ -8,7 +8,8 @@
<color name="main_accent_dark">#073642</color> <color name="main_accent_dark">#073642</color>
<color name="second_accent">#2aa198</color> <color name="second_accent">#2aa198</color>
<color name="second_accent_dark">#073642</color> <color name="second_accent_dark">#073642</color>
<color name="link">#2aa198</color> <color name="link">#268bd2</color>
<color name="link_visited">#2aa198</color>
<color name="url_bar">#fdf6e3</color> <color name="url_bar">#fdf6e3</color>
<color name="url_bar_loading">#586e75</color> <color name="url_bar_loading">#586e75</color>
</resources> </resources>

View file

@ -11,6 +11,7 @@
<color name="second_accent">#6c71c4</color> <color name="second_accent">#6c71c4</color>
<color name="second_accent_dark">#073642</color> <color name="second_accent_dark">#073642</color>
<color name="link">#268bd2</color> <color name="link">#268bd2</color>
<color name="link_visited">#2aa198</color>
<color name="url_bar">#002b36</color> <color name="url_bar">#002b36</color>
<color name="url_bar_loading">#fdf6e3</color> <color name="url_bar_loading">#fdf6e3</color>
<!-- <!--