For #4137 - Adds pagination to the history view
parent
0773069dad
commit
4494e40dbc
|
@ -0,0 +1,53 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.components.history
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.concept.storage.HistoryStorage
|
||||
import mozilla.components.concept.storage.VisitInfo
|
||||
import mozilla.components.concept.storage.VisitType
|
||||
|
||||
/**
|
||||
* An Interface for providing a paginated list of [VisitInfo]
|
||||
*/
|
||||
interface PagedHistoryProvider {
|
||||
/**
|
||||
* Gets a list of [VisitInfo]
|
||||
* @param offset How much to offset the list by
|
||||
* @param numberOfItems How many items to fetch
|
||||
* @param onComplete A callback that returns the list of [VisitInfo]
|
||||
*/
|
||||
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<VisitInfo>) -> Unit)
|
||||
}
|
||||
|
||||
// A PagedList DataSource runs on a background thread automatically.
|
||||
// If we run this in our own coroutineScope it breaks the PagedList
|
||||
fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider {
|
||||
return object : PagedHistoryProvider {
|
||||
override fun getHistory(
|
||||
offset: Long,
|
||||
numberOfItems: Long,
|
||||
onComplete: (List<VisitInfo>) -> Unit
|
||||
) {
|
||||
runBlocking {
|
||||
val history = this@createSynchronousPagedHistoryProvider.getVisitsPaginated(
|
||||
offset,
|
||||
numberOfItems,
|
||||
listOf(
|
||||
VisitType.NOT_A_VISIT,
|
||||
VisitType.DOWNLOAD,
|
||||
VisitType.REDIRECT_TEMPORARY,
|
||||
VisitType.RELOAD,
|
||||
VisitType.EMBED,
|
||||
VisitType.FRAMED_LINK,
|
||||
VisitType.REDIRECT_PERMANENT
|
||||
)
|
||||
)
|
||||
|
||||
onComplete(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,25 +8,14 @@ import android.content.Context
|
|||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||
import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
|
||||
import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
|
||||
import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
private sealed class AdapterItem {
|
||||
object DeleteButton : AdapterItem()
|
||||
data class SectionHeader(val range: Range) : AdapterItem()
|
||||
data class Item(val item: HistoryItem) : AdapterItem()
|
||||
}
|
||||
|
||||
private enum class Range {
|
||||
enum class HistoryItemTimeGroup {
|
||||
Today, ThisWeek, ThisMonth, Older;
|
||||
|
||||
fun humanReadable(context: Context): String = when (this) {
|
||||
|
@ -37,54 +26,41 @@ private enum class Range {
|
|||
}
|
||||
}
|
||||
|
||||
private class HistoryList(val history: List<HistoryItem>) {
|
||||
val items: List<AdapterItem>
|
||||
class HistoryAdapter(
|
||||
private val historyInteractor: HistoryInteractor
|
||||
) : PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback) {
|
||||
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||
|
||||
init {
|
||||
val oneDayAgo = getDaysAgo(zero_days).time
|
||||
val sevenDaysAgo = getDaysAgo(seven_days).time
|
||||
val thirtyDaysAgo = getDaysAgo(thirty_days).time
|
||||
override fun getItemViewType(position: Int): Int = HistoryListItemViewHolder.LAYOUT_ID
|
||||
|
||||
val lastWeek = LongRange(sevenDaysAgo, oneDayAgo)
|
||||
val lastMonth = LongRange(thirtyDaysAgo, sevenDaysAgo)
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
items.add(AdapterItem.DeleteButton)
|
||||
|
||||
val groups = history.groupBy { item ->
|
||||
when {
|
||||
DateUtils.isToday(item.visitedAt) -> Range.Today
|
||||
lastWeek.contains(item.visitedAt) -> Range.ThisWeek
|
||||
lastMonth.contains(item.visitedAt) -> Range.ThisMonth
|
||||
else -> Range.Older
|
||||
}
|
||||
}
|
||||
|
||||
items.addAll(groups.adapterItemsForRange(Range.Today))
|
||||
items.addAll(groups.adapterItemsForRange(Range.ThisWeek))
|
||||
items.addAll(groups.adapterItemsForRange(Range.ThisMonth))
|
||||
items.addAll(groups.adapterItemsForRange(Range.Older))
|
||||
// No history only the delete button, so let's clear the list to show the empty text
|
||||
if (items.size == 1) items.clear()
|
||||
this.items = items
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryListItemViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return HistoryListItemViewHolder(view, historyInteractor)
|
||||
}
|
||||
|
||||
private fun Map<Range, List<HistoryItem>>.adapterItemsForRange(range: Range): List<AdapterItem> {
|
||||
return this[range]?.let { historyItems ->
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
if (historyItems.isNotEmpty()) {
|
||||
items.add(AdapterItem.SectionHeader(range))
|
||||
for (item in historyItems) {
|
||||
items.add(AdapterItem.Item(item))
|
||||
}
|
||||
}
|
||||
items
|
||||
} ?: listOf()
|
||||
fun updateMode(mode: HistoryState.Mode) {
|
||||
this.mode = mode
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HistoryListItemViewHolder, position: Int) {
|
||||
val previous = if (position == 0) null else getItem(position - 1)
|
||||
val current = getItem(position) ?: return
|
||||
|
||||
val previousHeader = previous?.let(::timeGroupForHistoryItem)
|
||||
val currentHeader = timeGroupForHistoryItem(current)
|
||||
val timeGroup = if (currentHeader != previousHeader) currentHeader else null
|
||||
holder.bind(current, timeGroup, position == 0, mode)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val zero_days = 0
|
||||
private const val seven_days = 7
|
||||
private const val thirty_days = 30
|
||||
private const val zeroDays = 0
|
||||
private const val sevenDays = 7
|
||||
private const val thirtyDays = 30
|
||||
private val oneDayAgo = getDaysAgo(zeroDays).time
|
||||
private val sevenDaysAgo = getDaysAgo(sevenDays).time
|
||||
private val thirtyDaysAgo = getDaysAgo(thirtyDays).time
|
||||
private val lastWeekRange = LongRange(sevenDaysAgo, oneDayAgo)
|
||||
private val lastMonthRange = LongRange(thirtyDaysAgo, sevenDaysAgo)
|
||||
|
||||
private fun getDaysAgo(daysAgo: Int): Date {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
@ -92,96 +68,27 @@ private class HistoryList(val history: List<HistoryItem>) {
|
|||
|
||||
return calendar.time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var historyList: HistoryList = HistoryList(emptyList())
|
||||
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||
var selected = listOf<HistoryItem>()
|
||||
|
||||
fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) {
|
||||
val diffUtil = DiffUtil.calculateDiff(
|
||||
HistoryDiffUtil(
|
||||
this.historyList,
|
||||
HistoryList(items),
|
||||
HistoryList(selected),
|
||||
HistoryList((mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()),
|
||||
this.mode,
|
||||
mode
|
||||
)
|
||||
)
|
||||
|
||||
this.historyList = HistoryList(items)
|
||||
this.mode = mode
|
||||
this.selected = if (mode is HistoryState.Mode.Editing) mode.selectedItems else listOf()
|
||||
|
||||
diffUtil.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
private class HistoryDiffUtil(
|
||||
val old: HistoryList,
|
||||
val new: HistoryList,
|
||||
val oldSelected: HistoryList,
|
||||
val newSelected: HistoryList,
|
||||
val oldMode: HistoryState.Mode,
|
||||
val newMode: HistoryState.Mode
|
||||
) : DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old.items[oldItemPosition] == new.items[newItemPosition]
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val modesEqual = oldMode::class == newMode::class
|
||||
val isStillSelected =
|
||||
oldSelected.items.contains(old.items[oldItemPosition]) &&
|
||||
newSelected.items.contains(new.items[newItemPosition])
|
||||
val isStillNotSelected =
|
||||
!oldSelected.items.contains(old.items[oldItemPosition]) &&
|
||||
!newSelected.items.contains(new.items[newItemPosition])
|
||||
return modesEqual && (isStillSelected || isStillNotSelected)
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int = old.items.size
|
||||
override fun getNewListSize(): Int = new.items.size
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = historyList.items.size
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (historyList.items[position]) {
|
||||
is AdapterItem.DeleteButton -> HistoryDeleteButtonViewHolder.LAYOUT_ID
|
||||
is AdapterItem.SectionHeader -> HistoryHeaderViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Item -> HistoryListItemViewHolder.ID
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == HistoryListItemViewHolder.ID) {
|
||||
val view = LibrarySiteItemView(parent.context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
}
|
||||
HistoryListItemViewHolder(view, historyInteractor)
|
||||
} else {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
when (viewType) {
|
||||
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, historyInteractor)
|
||||
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
|
||||
else -> throw IllegalStateException()
|
||||
private fun timeGroupForHistoryItem(item: HistoryItem): HistoryItemTimeGroup {
|
||||
return when {
|
||||
DateUtils.isToday(item.visitedAt) -> HistoryItemTimeGroup.Today
|
||||
lastWeekRange.contains(item.visitedAt) -> HistoryItemTimeGroup.ThisWeek
|
||||
lastMonthRange.contains(item.visitedAt) -> HistoryItemTimeGroup.ThisMonth
|
||||
else -> HistoryItemTimeGroup.Older
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is HistoryDeleteButtonViewHolder -> holder.bind(mode)
|
||||
is HistoryHeaderViewHolder -> historyList.items[position].also {
|
||||
if (it is AdapterItem.SectionHeader) {
|
||||
holder.bind(it.range.humanReadable(holder.itemView.context))
|
||||
}
|
||||
private val historyDiffCallback = object : DiffUtil.ItemCallback<HistoryItem>() {
|
||||
override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
is HistoryListItemViewHolder -> (historyList.items[position] as AdapterItem.Item).also {
|
||||
holder.bind(it.item, mode)
|
||||
|
||||
override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: HistoryItem, newItem: HistoryItem): Any? {
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.history
|
||||
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import mozilla.components.concept.storage.VisitInfo
|
||||
import org.mozilla.fenix.components.history.PagedHistoryProvider
|
||||
import org.mozilla.fenix.ext.getHostFromUrl
|
||||
|
||||
class HistoryDataSource(
|
||||
private val historyProvider: PagedHistoryProvider
|
||||
) : ItemKeyedDataSource<Int, HistoryItem>() {
|
||||
override fun getKey(item: HistoryItem): Int = item.id
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<HistoryItem>
|
||||
) {
|
||||
historyProvider.getHistory(INITIAL_OFFSET, params.requestedLoadSize.toLong()) { history ->
|
||||
val items = history.mapIndexed(transformVisitInfoToHistoryItem(INITIAL_OFFSET.toInt()))
|
||||
callback.onResult(items)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) {
|
||||
historyProvider.getHistory(params.key.toLong(), params.requestedLoadSize.toLong()) { history ->
|
||||
val items = history.mapIndexed(transformVisitInfoToHistoryItem(params.key))
|
||||
callback.onResult(items)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) {}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_OFFSET = 0L
|
||||
|
||||
fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> HistoryItem {
|
||||
return { id, visit ->
|
||||
val title = visit.title
|
||||
?.takeIf(String::isNotEmpty)
|
||||
?: visit.url.getHostFromUrl()
|
||||
?: visit.url
|
||||
|
||||
HistoryItem(offset + id, title, visit.url, visit.visitTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.mozilla.fenix.library.history
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import org.mozilla.fenix.components.history.PagedHistoryProvider
|
||||
|
||||
class HistoryDataSourceFactory(
|
||||
private val historyProvider: PagedHistoryProvider
|
||||
) : DataSource.Factory<Int, HistoryItem>() {
|
||||
|
||||
val datasourceLiveData = MutableLiveData<HistoryDataSource>()
|
||||
|
||||
override fun create(): DataSource<Int, HistoryItem> {
|
||||
val datasource = HistoryDataSource(historyProvider)
|
||||
datasourceLiveData.postValue(datasource)
|
||||
return datasource
|
||||
}
|
||||
}
|
|
@ -18,14 +18,12 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.Navigation
|
||||
import kotlinx.android.synthetic.main.fragment_history.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.VisitType
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
|
@ -34,20 +32,19 @@ import org.mozilla.fenix.HomeActivity
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.getHostFromUrl
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.simplifiedUrl
|
||||
import org.mozilla.fenix.share.ShareTab
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class HistoryFragment : Fragment(), BackHandler {
|
||||
private lateinit var historyStore: HistoryStore
|
||||
private lateinit var historyView: HistoryView
|
||||
private lateinit var historyInteractor: HistoryInteractor
|
||||
private lateinit var viewModel: HistoryViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -70,6 +67,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
::deleteHistoryItems
|
||||
)
|
||||
historyView = HistoryView(view.history_layout, historyInteractor)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
|
@ -80,18 +78,24 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel = HistoryViewModel(
|
||||
requireComponents.core.historyStorage.createSynchronousPagedHistoryProvider()
|
||||
)
|
||||
|
||||
requireComponents.analytics.metrics.track(Event.HistoryOpened)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
fun deleteHistoryItems(items: List<HistoryItem>) {
|
||||
fun deleteHistoryItems(items: Set<HistoryItem>) {
|
||||
lifecycleScope.launch {
|
||||
val storage = context?.components?.core?.historyStorage
|
||||
for (item in items) {
|
||||
context?.components?.analytics?.metrics?.track(Event.HistoryItemRemoved)
|
||||
storage?.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
reloadData()
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryAction.ExitDeletionMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,7 +106,9 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
historyView.update(it)
|
||||
}
|
||||
|
||||
lifecycleScope.launch { reloadData() }
|
||||
viewModel.history.observe(this, Observer {
|
||||
historyView.historyAdapter.submitList(it)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -137,7 +143,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.share_history_multi_select -> {
|
||||
val selectedHistory =
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
|
||||
when {
|
||||
selectedHistory.size == 1 ->
|
||||
share(selectedHistory.first().url)
|
||||
|
@ -156,17 +162,18 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
R.id.delete_history_multi_select -> {
|
||||
val components = context?.applicationContext?.components!!
|
||||
val selectedHistory =
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
deleteSelectedHistory(selectedHistory, components)
|
||||
reloadData()
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryAction.ExitDeletionMode)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.open_history_in_new_tabs_multi_select -> {
|
||||
val selectedHistory =
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
|
||||
requireComponents.useCases.tabsUseCases.addTab.let { useCase ->
|
||||
for (selectedItem in selectedHistory) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
||||
|
@ -186,7 +193,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
}
|
||||
R.id.open_history_in_private_tabs_multi_select -> {
|
||||
val selectedHistory =
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
|
||||
requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase ->
|
||||
for (selectedItem in selectedHistory) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
||||
|
@ -230,8 +237,8 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
lifecycleScope.launch {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
|
||||
requireComponents.core.historyStorage.deleteEverything()
|
||||
reloadData()
|
||||
launch(Dispatchers.Main) {
|
||||
launch(Main) {
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryAction.ExitDeletionMode)
|
||||
}
|
||||
}
|
||||
|
@ -243,51 +250,8 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun reloadData() {
|
||||
val excludeTypes = listOf(
|
||||
VisitType.NOT_A_VISIT,
|
||||
VisitType.DOWNLOAD,
|
||||
VisitType.REDIRECT_TEMPORARY,
|
||||
VisitType.RELOAD,
|
||||
VisitType.EMBED,
|
||||
VisitType.FRAMED_LINK,
|
||||
VisitType.REDIRECT_PERMANENT
|
||||
)
|
||||
|
||||
// Until we have proper pagination, only display a limited set of history to avoid blowing up the UI.
|
||||
// See https://github.com/mozilla-mobile/fenix/issues/1393
|
||||
val sinceTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(HISTORY_TIME_DAYS)
|
||||
var previous: String? = null
|
||||
|
||||
val items = requireComponents.core.historyStorage
|
||||
.getDetailedVisits(sinceTimeMs, excludeTypes = excludeTypes)
|
||||
// We potentially have a large amount of visits, and multiple processing steps.
|
||||
// Wrapping iterator in a sequence should make this a little memory-more efficient.
|
||||
.asSequence()
|
||||
.sortedByDescending { it.visitTime }
|
||||
.filter {
|
||||
val current = it.url.simplifiedUrl()
|
||||
val isNotDuplicate = current != previous
|
||||
previous = current
|
||||
isNotDuplicate
|
||||
}
|
||||
.mapIndexed { id, item ->
|
||||
val title = item.title
|
||||
?.takeIf(String::isNotEmpty)
|
||||
?: item.url.getHostFromUrl()
|
||||
?: item.url
|
||||
|
||||
HistoryItem(id, title, item.url, item.visitTime)
|
||||
}
|
||||
.toList()
|
||||
|
||||
withContext(Main) {
|
||||
historyStore.dispatch(HistoryAction.Change(items))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSelectedHistory(
|
||||
selected: List<HistoryItem>,
|
||||
selected: Set<HistoryItem>,
|
||||
components: Components = requireComponents
|
||||
) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
|
@ -306,8 +270,4 @@ class HistoryFragment : Fragment(), BackHandler {
|
|||
)
|
||||
nav(R.id.historyFragment, directions)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HISTORY_TIME_DAYS = 3L
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,26 +13,43 @@ class HistoryInteractor(
|
|||
private val openToBrowser: (item: HistoryItem) -> Unit,
|
||||
private val displayDeleteAll: () -> Unit,
|
||||
private val invalidateOptionsMenu: () -> Unit,
|
||||
private val deleteHistoryItems: (List<HistoryItem>) -> Unit
|
||||
private val deleteHistoryItems: (Set<HistoryItem>) -> Unit
|
||||
) : HistoryViewInteractor {
|
||||
override fun onHistoryItemOpened(item: HistoryItem) {
|
||||
openToBrowser(item)
|
||||
override fun onItemPress(item: HistoryItem) {
|
||||
val mode = store.state.mode
|
||||
when (mode) {
|
||||
is HistoryState.Mode.Normal -> openToBrowser(item)
|
||||
is HistoryState.Mode.Editing -> {
|
||||
val isSelected = mode.selectedItems.contains(item)
|
||||
|
||||
if (isSelected) {
|
||||
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
|
||||
} else {
|
||||
store.dispatch(HistoryAction.AddItemForRemoval(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEnterEditMode(selectedItem: HistoryItem) {
|
||||
store.dispatch(HistoryAction.EnterEditMode(selectedItem))
|
||||
override fun onItemLongPress(item: HistoryItem) {
|
||||
val isSelected = (store.state.mode as? HistoryState.Mode.Editing)?.let {
|
||||
it.selectedItems.contains(item)
|
||||
} ?: false
|
||||
|
||||
if (isSelected) {
|
||||
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
|
||||
} else {
|
||||
store.dispatch(HistoryAction.AddItemForRemoval(item))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
store.dispatch(HistoryAction.ExitEditMode)
|
||||
}
|
||||
|
||||
override fun onItemAddedForRemoval(item: HistoryItem) {
|
||||
store.dispatch(HistoryAction.AddItemForRemoval(item))
|
||||
}
|
||||
|
||||
override fun onItemRemovedForRemoval(item: HistoryItem) {
|
||||
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
|
||||
override fun onBackPressed(): Boolean {
|
||||
return if (store.state.mode is HistoryState.Mode.Editing) {
|
||||
store.dispatch(HistoryAction.ExitEditMode)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onModeSwitched() {
|
||||
|
@ -44,10 +61,10 @@ class HistoryInteractor(
|
|||
}
|
||||
|
||||
override fun onDeleteOne(item: HistoryItem) {
|
||||
deleteHistoryItems.invoke(listOf(item))
|
||||
deleteHistoryItems.invoke(setOf(item))
|
||||
}
|
||||
|
||||
override fun onDeleteSome(items: List<HistoryItem>) {
|
||||
override fun onDeleteSome(items: Set<HistoryItem>) {
|
||||
deleteHistoryItems.invoke(items)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ class HistoryStore(initialState: HistoryState) :
|
|||
*/
|
||||
sealed class HistoryAction : Action {
|
||||
data class Change(val list: List<HistoryItem>) : HistoryAction()
|
||||
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
|
||||
object ExitEditMode : HistoryAction()
|
||||
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
|
||||
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
|
||||
|
@ -44,7 +43,7 @@ sealed class HistoryAction : Action {
|
|||
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
|
||||
sealed class Mode {
|
||||
object Normal : Mode()
|
||||
data class Editing(val selectedItems: List<HistoryItem>) : Mode()
|
||||
data class Editing(val selectedItems: Set<HistoryItem>) : Mode()
|
||||
object Deleting : Mode()
|
||||
}
|
||||
}
|
||||
|
@ -55,28 +54,21 @@ data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
|
|||
fun historyStateReducer(state: HistoryState, action: HistoryAction): HistoryState {
|
||||
return when (action) {
|
||||
is HistoryAction.Change -> state.copy(mode = HistoryState.Mode.Normal, items = action.list)
|
||||
is HistoryAction.EnterEditMode -> state.copy(
|
||||
mode = HistoryState.Mode.Editing(listOf(action.item))
|
||||
)
|
||||
is HistoryAction.AddItemForRemoval -> {
|
||||
val mode = state.mode
|
||||
if (mode is HistoryState.Mode.Editing) {
|
||||
val items = mode.selectedItems + listOf(action.item)
|
||||
val items = mode.selectedItems + setOf(action.item)
|
||||
state.copy(mode = HistoryState.Mode.Editing(items))
|
||||
} else {
|
||||
state
|
||||
state.copy(mode = HistoryState.Mode.Editing(setOf(action.item)))
|
||||
}
|
||||
}
|
||||
is HistoryAction.RemoveItemForRemoval -> {
|
||||
var mode = state.mode
|
||||
|
||||
if (mode is HistoryState.Mode.Editing) {
|
||||
val items = mode.selectedItems.filter { it.id != action.item.id }
|
||||
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(
|
||||
items
|
||||
)
|
||||
|
||||
state.copy(mode = mode)
|
||||
val items = mode.selectedItems.minus(action.item)
|
||||
state.copy(mode = HistoryState.Mode.Editing(items))
|
||||
} else {
|
||||
state
|
||||
}
|
||||
|
|
|
@ -14,12 +14,10 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_history.*
|
||||
import kotlinx.android.synthetic.main.component_history.view.*
|
||||
import kotlinx.android.synthetic.main.delete_history_button.*
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.asActivity
|
||||
|
@ -31,33 +29,19 @@ import org.mozilla.fenix.ext.getColorResFromAttr
|
|||
*/
|
||||
interface HistoryViewInteractor {
|
||||
/**
|
||||
* Called whenever a history item is tapped to open that history entry in the browser
|
||||
* @param item the history item to open in browser
|
||||
* Called when a user taps a history item
|
||||
*/
|
||||
fun onHistoryItemOpened(item: HistoryItem)
|
||||
fun onItemPress(item: HistoryItem)
|
||||
|
||||
/**
|
||||
* Called when a history item is long pressed and edit mode is launched
|
||||
* @param selectedItem the history item to start selected for deletion in edit mode
|
||||
* Called when a user long clicks a user
|
||||
*/
|
||||
fun onEnterEditMode(selectedItem: HistoryItem)
|
||||
fun onItemLongPress(item: HistoryItem)
|
||||
|
||||
/**
|
||||
* Called on backpressed to exit edit mode
|
||||
*/
|
||||
fun onBackPressed()
|
||||
|
||||
/**
|
||||
* Called when a history item is tapped in edit mode and added for removal
|
||||
* @param item the history item to add to selected items for deletion in edit mode
|
||||
*/
|
||||
fun onItemAddedForRemoval(item: HistoryItem)
|
||||
|
||||
/**
|
||||
* Called when a selected history item is tapped in edit mode and removed from removal
|
||||
* @param item the history item to remove from the selected items for deletion in edit mode
|
||||
*/
|
||||
fun onItemRemovedForRemoval(item: HistoryItem)
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
/**
|
||||
* Called when the mode is switched so we can invalidate the menu
|
||||
|
@ -79,7 +63,7 @@ interface HistoryViewInteractor {
|
|||
* Called when multiple history items are deleted
|
||||
* @param items the history items to delete
|
||||
*/
|
||||
fun onDeleteSome(items: List<HistoryItem>)
|
||||
fun onDeleteSome(items: Set<HistoryItem>)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,18 +81,20 @@ class HistoryView(
|
|||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
private val historyAdapter: HistoryAdapter
|
||||
val historyAdapter: HistoryAdapter
|
||||
private var items: List<HistoryItem> = listOf()
|
||||
private val context = container.context
|
||||
var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||
private set
|
||||
private val activity = context?.asActivity()
|
||||
|
||||
private val layoutManager = LinearLayoutManager(container.context)
|
||||
init {
|
||||
historyAdapter = HistoryAdapter(interactor)
|
||||
|
||||
view.history_list.apply {
|
||||
historyAdapter = HistoryAdapter(interactor)
|
||||
layoutManager = this@HistoryView.layoutManager
|
||||
adapter = historyAdapter
|
||||
layoutManager = LinearLayoutManager(container.context)
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,17 +103,33 @@ class HistoryView(
|
|||
if (state.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
|
||||
|
||||
if (state.mode != mode) {
|
||||
mode = state.mode
|
||||
interactor.onModeSwitched()
|
||||
historyAdapter.updateMode(state.mode)
|
||||
|
||||
val oldMode = mode
|
||||
if (oldMode is HistoryState.Mode.Editing) {
|
||||
oldMode.selectedItems.forEach {
|
||||
historyAdapter.notifyItemChanged(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(view.history_list.adapter as HistoryAdapter).updateData(state.items, state.mode)
|
||||
(state.mode as? HistoryState.Mode.Editing)?.also {
|
||||
val oldMode = (mode as? HistoryState.Mode.Editing)
|
||||
val unselectedItems = oldMode?.selectedItems?.minus(it.selectedItems) ?: setOf()
|
||||
|
||||
it.selectedItems.union(unselectedItems).forEach { item ->
|
||||
historyAdapter.notifyItemChanged(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
items = state.items
|
||||
when (val mode = mode) {
|
||||
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty())
|
||||
when (val mode = state.mode) {
|
||||
is HistoryState.Mode.Normal -> setUIForNormalMode()
|
||||
is HistoryState.Mode.Editing -> setUIForSelectingMode(mode.selectedItems.size)
|
||||
}
|
||||
|
||||
mode = state.mode
|
||||
}
|
||||
|
||||
private fun setUIForSelectingMode(selectedItemSize: Int) {
|
||||
|
@ -139,10 +141,10 @@ class HistoryView(
|
|||
)
|
||||
}
|
||||
|
||||
private fun setUIForNormalMode(isEmpty: Boolean) {
|
||||
private fun setUIForNormalMode() {
|
||||
activity?.title = context.getString(R.string.library_history)
|
||||
delete_history_button?.isVisible = !isEmpty
|
||||
history_empty_view.isVisible = isEmpty
|
||||
// history_list?.isVisible = !isEmpty
|
||||
// history_empty_view.isVisible = isEmpty
|
||||
setToolbarColors(
|
||||
context!!.getColorResFromAttr(R.attr.primaryText),
|
||||
context.getColorResFromAttr(R.attr.foundation)
|
||||
|
@ -186,15 +188,6 @@ class HistoryView(
|
|||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return when (mode) {
|
||||
is HistoryState.Mode.Editing -> {
|
||||
mode = HistoryState.Mode.Normal
|
||||
historyAdapter.updateData(items, mode)
|
||||
setUIForNormalMode(items.isEmpty())
|
||||
interactor.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
return interactor.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.history
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import org.mozilla.fenix.components.history.PagedHistoryProvider
|
||||
|
||||
class HistoryViewModel(historyProvider: PagedHistoryProvider) : ViewModel() {
|
||||
var history: LiveData<PagedList<HistoryItem>>
|
||||
private val datasource: LiveData<HistoryDataSource>
|
||||
|
||||
init {
|
||||
val historyDataSourceFactory = HistoryDataSourceFactory(historyProvider)
|
||||
datasource = historyDataSourceFactory.datasourceLiveData
|
||||
|
||||
history = LivePagedListBuilder(historyDataSourceFactory, PAGE_SIZE).build()
|
||||
}
|
||||
|
||||
fun invalidate() {
|
||||
datasource.value?.invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_SIZE = 25
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.history.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.delete_history_button.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||
import org.mozilla.fenix.library.history.HistoryState
|
||||
|
||||
class HistoryDeleteButtonViewHolder(
|
||||
view: View,
|
||||
historyInteractor: HistoryInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private var mode: HistoryState.Mode? = null
|
||||
private val buttonView = view.delete_history_button
|
||||
|
||||
init {
|
||||
buttonView.setOnClickListener {
|
||||
mode?.also {
|
||||
when (it) {
|
||||
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
|
||||
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(mode: HistoryState.Mode) {
|
||||
this.mode = mode
|
||||
|
||||
buttonView.run {
|
||||
val isDeleting = mode is HistoryState.Mode.Deleting
|
||||
if (isDeleting || mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
|
||||
isEnabled = false
|
||||
alpha = DISABLED_ALPHA
|
||||
} else {
|
||||
isEnabled = true
|
||||
alpha = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DISABLED_ALPHA = 0.4f
|
||||
const val LAYOUT_ID = R.layout.delete_history_button
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.library.history.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.history_header.view.*
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class HistoryHeaderViewHolder(
|
||||
view: View
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private val title = view.history_header_title
|
||||
|
||||
fun bind(title: String) {
|
||||
this.title.text = title
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.history_header
|
||||
}
|
||||
}
|
|
@ -4,19 +4,36 @@
|
|||
|
||||
package org.mozilla.fenix.library.history.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ThemeManager
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||
import org.mozilla.fenix.library.history.HistoryItem
|
||||
import org.mozilla.fenix.library.history.HistoryItemMenu
|
||||
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
|
||||
import org.mozilla.fenix.library.history.HistoryState
|
||||
|
||||
class HistoryListItemViewHolder(
|
||||
val view: LibrarySiteItemView,
|
||||
private val view: View,
|
||||
private val historyInteractor: HistoryInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val layout = view.history_layout
|
||||
private val favicon = view.history_favicon
|
||||
private val title = view.history_title
|
||||
private val url = view.history_url
|
||||
private val menuButton = view.history_item_overflow
|
||||
private val headerWrapper = view.header_wrapper
|
||||
private val headerTitle = view.header_title
|
||||
private val deleteButtonWrapper = view.delete_button_wrapper
|
||||
private val deleteButton = view.delete_button
|
||||
|
||||
private var item: HistoryItem? = null
|
||||
private lateinit var historyMenu: HistoryItemMenu
|
||||
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||
|
@ -24,41 +41,122 @@ class HistoryListItemViewHolder(
|
|||
init {
|
||||
setupMenu()
|
||||
|
||||
view.setOnLongClickListener {
|
||||
layout.setOnLongClickListener {
|
||||
item?.apply {
|
||||
historyInteractor.onEnterEditMode(this)
|
||||
historyInteractor.onItemLongPress(this)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
view.overflowView.setOnClickListener {
|
||||
menuButton.setOnClickListener {
|
||||
historyMenu.menuBuilder.build(view.context).show(
|
||||
anchor = it,
|
||||
orientation = BrowserMenu.Orientation.DOWN
|
||||
)
|
||||
}
|
||||
|
||||
view.displayAs(LibrarySiteItemView.ItemType.SITE)
|
||||
itemView.history_layout.setOnClickListener {
|
||||
item?.also(historyInteractor::onItemPress)
|
||||
}
|
||||
|
||||
deleteButton.setOnClickListener {
|
||||
mode?.also {
|
||||
when (it) {
|
||||
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
|
||||
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: HistoryItem, mode: HistoryState.Mode) {
|
||||
fun bind(
|
||||
item: HistoryItem,
|
||||
timeGroup: HistoryItemTimeGroup?,
|
||||
showDeletebutton: Boolean,
|
||||
mode: HistoryState.Mode
|
||||
) {
|
||||
this.item = item
|
||||
this.mode = mode
|
||||
|
||||
view.titleView.text = item.title
|
||||
view.urlView.text = item.url
|
||||
title.text = item.title
|
||||
url.text = item.url
|
||||
|
||||
val selected = mode is HistoryState.Mode.Editing && mode.selectedItems.contains(item)
|
||||
toggleDeleteButton(showDeletebutton, mode)
|
||||
|
||||
setClickListeners(item, selected)
|
||||
val headerText = timeGroup?.let { it.humanReadable(view.context) }
|
||||
toggleHeader(headerText)
|
||||
|
||||
view.changeSelected(selected)
|
||||
view.loadFavicon(item.url)
|
||||
val selected = toggleSelected(mode, item)
|
||||
|
||||
if (mode is HistoryState.Mode.Editing) {
|
||||
val backgroundTint =
|
||||
if (selected) {
|
||||
ThemeManager.resolveAttribute(R.attr.accentHighContrast, itemView.context)
|
||||
} else {
|
||||
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
||||
}
|
||||
val backgroundTintList =
|
||||
ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
||||
favicon.backgroundTintList = backgroundTintList
|
||||
|
||||
if (selected) {
|
||||
favicon.setImageResource(R.drawable.mozac_ic_check)
|
||||
} else {
|
||||
updateFavIcon(item.url)
|
||||
}
|
||||
} else {
|
||||
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
||||
val backgroundTintList =
|
||||
ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
||||
favicon.backgroundTintList = backgroundTintList
|
||||
updateFavIcon(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSelected(
|
||||
mode: HistoryState.Mode,
|
||||
item: HistoryItem
|
||||
): Boolean {
|
||||
return when (mode) {
|
||||
is HistoryState.Mode.Editing -> mode.selectedItems.contains(item)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleHeader(text: String?) {
|
||||
text?.also {
|
||||
headerWrapper.visibility = View.VISIBLE
|
||||
headerTitle.text = it
|
||||
} ?: run {
|
||||
headerWrapper.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleDeleteButton(
|
||||
showDeletebutton: Boolean,
|
||||
mode: HistoryState.Mode
|
||||
) {
|
||||
if (showDeletebutton) {
|
||||
deleteButtonWrapper.visibility = View.VISIBLE
|
||||
|
||||
deleteButton.run {
|
||||
val isDeleting = mode is HistoryState.Mode.Deleting
|
||||
if (isDeleting || mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
|
||||
isEnabled = false
|
||||
alpha = DELETE_BUTTON_DISABLED_ALPHA
|
||||
} else {
|
||||
isEnabled = true
|
||||
alpha = 1f
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deleteButtonWrapper.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
historyMenu = HistoryItemMenu(view.context) {
|
||||
this.historyMenu = HistoryItemMenu(itemView.context) {
|
||||
when (it) {
|
||||
is HistoryItemMenu.Item.Delete -> {
|
||||
item?.apply { historyInteractor.onDeleteOne(this) }
|
||||
|
@ -67,20 +165,12 @@ class HistoryListItemViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setClickListeners(
|
||||
item: HistoryItem,
|
||||
selected: Boolean
|
||||
) {
|
||||
view.setOnClickListener {
|
||||
when {
|
||||
mode == HistoryState.Mode.Normal -> historyInteractor.onHistoryItemOpened(item)
|
||||
selected -> historyInteractor.onItemRemovedForRemoval(item)
|
||||
else -> historyInteractor.onItemAddedForRemoval(item)
|
||||
}
|
||||
}
|
||||
private fun updateFavIcon(url: String) {
|
||||
favicon.context.components.core.icons.loadIntoView(favicon, url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = LibrarySiteItemView.ItemType.SITE.ordinal
|
||||
const val DELETE_BUTTON_DISABLED_ALPHA = 0.4f
|
||||
const val LAYOUT_ID = R.layout.history_list_item
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_history_button"
|
||||
style="@style/ThemeIndependentMaterialGreyButtonDestructive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/history_delete_all"
|
||||
app:rippleColor="?secondaryText" />
|
||||
</FrameLayout>
|
|
@ -2,14 +2,9 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/history_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.mozilla.fenix.library.history.HistoryFragment">
|
||||
<LinearLayout
|
||||
android:id="@+id/history_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"/>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
android:orientation="vertical"/>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_header_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="17sp"
|
||||
android:textColor="?primaryText"/>
|
||||
</FrameLayout>
|
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical">
|
||||
<FrameLayout
|
||||
android:id="@+id/delete_button_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="gone">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_button"
|
||||
style="@style/ThemeIndependentMaterialGreyButtonDestructive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/history_delete_all"
|
||||
app:rippleColor="?secondaryText" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/header_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:id="@+id/header_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="17sp"
|
||||
android:textColor="?primaryText"/>
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/history_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="4dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="0dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/history_item_overflow"
|
||||
android:layout_width="@dimen/glyph_button_width"
|
||||
android:layout_height="@dimen/glyph_button_height"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/content_description_history_menu"
|
||||
android:src="@drawable/ic_menu"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/history_favicon"
|
||||
android:layout_width="@dimen/history_favicon_width_height"
|
||||
android:layout_height="@dimen/history_favicon_width_height"
|
||||
android:background="@drawable/favicon_background"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_url"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
|
||||
app:layout_constraintStart_toEndOf="@id/history_favicon"
|
||||
app:layout_constraintTop_toBottomOf="@id/history_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
|
||||
app:layout_constraintStart_toEndOf="@id/history_favicon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
|
@ -28,7 +28,7 @@ object Versions {
|
|||
const val androidx_testing = "1.1.0-alpha08"
|
||||
const val androidx_test_ext = "1.0.0"
|
||||
const val androidx_core = "1.1.0-rc02"
|
||||
const val androidx_paging = "2.0.0"
|
||||
const val androidx_paging = "2.1.0"
|
||||
const val androidx_transition = "1.1.0"
|
||||
const val androidx_work = "2.0.1"
|
||||
const val google_material = "1.1.0-alpha07"
|
||||
|
@ -159,7 +159,7 @@ object Deps {
|
|||
const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
|
||||
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}"
|
||||
const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}"
|
||||
const val androidx_paging = "androidx.paging:paging-runtime:${Versions.androidx_paging}"
|
||||
const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}"
|
||||
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
|
||||
const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}"
|
||||
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"
|
||||
|
|
Loading…
Reference in New Issue