HistoryFragment: basic history browsable list

Have a card for each entry, rendered from last visited first to oldest.
Will probably start choking once the history has too many entries.
This commit is contained in:
dece 2022-01-24 20:09:04 +01:00
parent 473b1e012c
commit 8231a7080a
12 changed files with 226 additions and 26 deletions

View file

@ -5,6 +5,17 @@ Comet is a Gemini browser for Android, compatible back to Android 7.0.
Features
--------
- Developed using standard Android SDK practices for an intuitive experience.
- Uses the TLS capabilities of your device.
- Basic browsing capabilities, like click on links to visit them, wow!
- Streaming for all content types.
- History.
About About
----- -----

View file

@ -56,8 +56,10 @@ 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.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 'com.google.android.material:material:1.5.0' implementation 'com.google.android.material:material:1.5.0'
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View file

@ -38,5 +38,7 @@ object History {
dao.update(entry.also { it.title = title; it.lastVisit = now }) dao.update(entry.also { it.title = title; it.lastVisit = now })
} }
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

@ -0,0 +1,64 @@
package dev.lowrespalmtree.comet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lowrespalmtree.comet.History.HistoryEntry
import dev.lowrespalmtree.comet.HistoryItemAdapter.HistoryItemAdapterListener
import dev.lowrespalmtree.comet.databinding.FragmentHistoryListBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
class HistoryFragment : Fragment(), HistoryItemAdapterListener {
private lateinit var binding: FragmentHistoryListBinding
private lateinit var historyViewModel: HistoryViewModel
private lateinit var adapter: HistoryItemAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHistoryListBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
adapter = HistoryItemAdapter(this)
binding.list.layoutManager = LinearLayoutManager(requireContext())
binding.list.adapter = adapter
historyViewModel.items.observe(viewLifecycleOwner, { adapter.setItems(it) })
historyViewModel.refreshHistory()
}
override fun onItemClick(url: String) {
val bundle = bundleOf("url" to url)
findNavController().navigate(R.id.action_global_pageViewFragment, bundle)
}
@ExperimentalCoroutinesApi
class HistoryViewModel : ViewModel() {
val items: MutableLiveData<List<HistoryEntry>>
by lazy { MutableLiveData<List<HistoryEntry>>() }
fun refreshHistory() {
viewModelScope.launch(Dispatchers.IO) {
items.postValue(History.getAll())
}
}
}
}

View file

@ -0,0 +1,47 @@
package dev.lowrespalmtree.comet
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import dev.lowrespalmtree.comet.History.HistoryEntry
import dev.lowrespalmtree.comet.databinding.FragmentHistoryItemBinding
class HistoryItemAdapter(private val listener: HistoryItemAdapterListener) :
RecyclerView.Adapter<HistoryItemAdapter.ViewHolder>() {
private var items = listOf<HistoryEntry>()
interface HistoryItemAdapterListener {
fun onItemClick(url: String)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
FragmentHistoryItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.binding.uriText.text = item.uri
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) }
}
override fun getItemCount(): Int = items.size
@SuppressLint("NotifyDataSetChanged")
fun setItems(items: List<HistoryEntry>) {
this.items = items.toList()
notifyDataSetChanged()
}
inner class ViewHolder(val binding: FragmentHistoryItemBinding) :
RecyclerView.ViewHolder(binding.root)
}

View file

@ -1,16 +1,19 @@
package dev.lowrespalmtree.comet package dev.lowrespalmtree.comet
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
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
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private var navHost: NavHostFragment? = null private var nhf: NavHostFragment? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -19,22 +22,15 @@ class MainActivity : AppCompatActivity() {
Database.init(applicationContext) // TODO move to App Startup? Database.init(applicationContext) // TODO move to App Startup?
navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment? nhf = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
navHost?.also { binding.drawerNavigation.setupWithNavController(it.navController) } nhf?.also { binding.drawerNavigation.setupWithNavController(it.navController) }
} }
fun goHome() { /** Navigate to the PageViewFragment; this will automatically use the home URL if any. */
navHost?.navController?.navigate(R.id.action_global_pageViewFragment) fun goHome(item: MenuItem) {
binding.drawerLayout.closeDrawers() val bundle = bundleOf()
} Preferences.getHomeUrl(this)?.let { bundle.putString("url", it) }
nhf?.navController?.navigate(R.id.action_global_pageViewFragment, bundle)
fun openHistory() {
binding.drawerLayout.closeDrawers()
// TODO
}
fun openSettings() {
navHost?.navController?.navigate(R.id.action_global_settingsFragment)
binding.drawerLayout.closeDrawers() binding.drawerLayout.closeDrawers()
} }
} }

View file

@ -22,11 +22,12 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
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.databinding.FragmentPageViewBinding import dev.lowrespalmtree.comet.databinding.FragmentPageViewBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class PageViewFragment : Fragment(), ContentAdapter.ContentAdapterListener { class PageViewFragment : Fragment(), ContentAdapterListener {
private lateinit var binding: FragmentPageViewBinding private lateinit var binding: FragmentPageViewBinding
private lateinit var pageViewModel: PageViewModel private lateinit var pageViewModel: PageViewModel
private lateinit var adapter: ContentAdapter private lateinit var adapter: ContentAdapter
@ -64,9 +65,11 @@ class PageViewFragment : Fragment(), ContentAdapter.ContentAdapterListener {
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() } activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner) { onBackPressed() }
if (visitedUrls.isEmpty()) { val url = arguments?.getString("url")
PreferenceManager.getDefaultSharedPreferences(requireContext()) if (!url.isNullOrEmpty()) {
.getString("home", null)?.let { openUrl(it) } openUrl(url)
} else if (visitedUrls.isEmpty()) {
Preferences.getHomeUrl(requireContext())?.let { openUrl(it) }
} }
} }

View file

@ -0,0 +1,9 @@
package dev.lowrespalmtree.comet
import android.content.Context
import androidx.preference.PreferenceManager
object Preferences {
fun getHomeUrl(context: Context): String? =
PreferenceManager.getDefaultSharedPreferences(context).getString("home", null)
}

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
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:textColor="@color/text"
android:layout_margin="8dp"
android:fontFamily="@font/preformatted"
android:typeface="monospace" />
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:name="dev.lowrespalmtree.comet.HistoryFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".HistoryFragment"
tools:listitem="@layout/fragment_history_item" />

View file

@ -1,17 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/home" android:id="@+id/homeItem"
android:icon="@android:drawable/ic_menu_myplaces" android:icon="@android:drawable/ic_menu_myplaces"
android:title="@string/home" android:onClick="goHome"
android:onClick="goHome" /> android:title="@string/home" />
<item <item
android:id="@+id/history" android:id="@+id/historyFragment"
android:icon="@android:drawable/ic_menu_recent_history" android:icon="@android:drawable/ic_menu_recent_history"
android:title="@string/history" /> android:title="@string/history" />
<item <item
android:id="@+id/settings" android:id="@+id/settingsFragment"
android:icon="@android:drawable/ic_menu_preferences" android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/settings" android:title="@string/settings" />
android:onClick="openSettings" />
</menu> </menu>

View file

@ -15,6 +15,17 @@
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" />
<fragment
android:id="@+id/historyFragment"
android:name="dev.lowrespalmtree.comet.HistoryFragment"
android:label="HistoryFragment" />
<action
android:id="@+id/action_global_historyFragment"
app:destination="@id/historyFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<fragment <fragment
android:id="@+id/settingsFragment" android:id="@+id/settingsFragment"
android:name="dev.lowrespalmtree.comet.SettingsFragment" android:name="dev.lowrespalmtree.comet.SettingsFragment"