1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2886 All content | Enhancement | Paging (#1621)

This commit is contained in:
Konstantin Ivanov 2024-10-02 23:34:19 +02:00 committed by GitHub
parent 4018a139b5
commit fa4bebb4ad
Signed by: github
GPG key ID: B5690EEEBB952194
9 changed files with 341 additions and 327 deletions

View file

@ -18,27 +18,11 @@ import com.anytypeio.anytype.presentation.objects.getProperName
import com.anytypeio.anytype.presentation.objects.getProperType
//region STATE
sealed class AllContentState {
data object Init : AllContentState()
data class Default(
val activeTab: AllContentTab,
val activeMode: AllContentMode,
val activeSort: AllContentSort,
val filter: String,
val limit: Int
) : AllContentState()
}
@Immutable
enum class AllContentTab {
PAGES, LISTS, MEDIA, BOOKMARKS, FILES, TYPES
}
sealed class AllContentMode {
data object AllContent : AllContentMode()
data object Unlinked : AllContentMode()
}
sealed class AllContentMenuMode {
abstract val isSelected: Boolean
@ -89,12 +73,6 @@ sealed class UiTitleState {
data object OnlyUnlinked : UiTitleState()
}
//MENU BUTTON
sealed class MenuButtonViewState {
data object Hidden : MenuButtonViewState()
data object Visible : MenuButtonViewState()
}
// TABS
@Immutable
sealed class UiTabsState {
@ -109,19 +87,13 @@ sealed class UiTabsState {
// CONTENT
sealed class UiContentState {
data object Hidden : UiContentState()
data object Loading : UiContentState()
data class Idle(val scrollToTop: Boolean = false) : UiContentState()
data object InitLoading : UiContentState()
data object Paging : UiContentState()
data object Empty : UiContentState()
data class Error(
val message: String,
) : UiContentState()
@Immutable
data class Content(
val items: List<UiContentItem>,
) : UiContentState()
}
// ITEMS
@ -160,25 +132,21 @@ sealed class UiContentItem {
// MENU
@Immutable
data class UiMenuState(
val mode: List<AllContentMenuMode>,
val container: MenuSortsItem.Container,
val sorts: List<MenuSortsItem.Sort>,
val types: List<MenuSortsItem.SortType>,
val showBin: Boolean = true
) {
companion object {
fun empty(): UiMenuState {
return UiMenuState(
mode = emptyList(),
container = MenuSortsItem.Container(AllContentSort.ByName()),
sorts = emptyList(),
types = emptyList()
)
}
}
sealed class UiMenuState{
data object Hidden : UiMenuState()
@Immutable
data class Visible(
val mode: List<AllContentMenuMode>,
val container: MenuSortsItem.Container,
val sorts: List<MenuSortsItem.Sort>,
val types: List<MenuSortsItem.SortType>,
val showBin: Boolean = true
) : UiMenuState()
}
sealed class MenuSortsItem {
data class Container(val sort: AllContentSort) : MenuSortsItem()
data class Sort(val sort: AllContentSort) : MenuSortsItem()
@ -192,20 +160,6 @@ sealed class MenuSortsItem {
//endregion
//region MAPPING
fun AllContentState.Default.toMenuMode(): AllContentMenuMode {
return when (activeMode) {
AllContentMode.AllContent -> AllContentMenuMode.AllContent(isSelected = true)
AllContentMode.Unlinked -> AllContentMenuMode.Unlinked(isSelected = true)
}
}
fun AllContentMode.view(): UiTitleState {
return when (this) {
AllContentMode.AllContent -> UiTitleState.AllContent
AllContentMode.Unlinked -> UiTitleState.OnlyUnlinked
}
}
fun Key?.mapRelationKeyToSort(): AllContentSort {
return when (this) {
Relations.CREATED_DATE -> AllContentSort.ByDateCreated()

View file

@ -38,7 +38,7 @@ val allContentTabLayouts = mapOf(
// Function to create subscription params
fun createSubscriptionParams(
spaceId: Id,
activeMode: AllContentMode,
activeMode: UiTitleState,
activeTab: AllContentTab,
activeSort: AllContentSort,
limitedObjectIds: List<String>,
@ -90,7 +90,7 @@ fun AllContentTab.filtersForSubscribe(
spaces: List<Id>,
activeSort: AllContentSort,
limitedObjectIds: List<Id>,
activeMode: AllContentMode
activeMode: UiTitleState
): Pair<List<DVFilter>, List<DVSort>> {
val tab = this
when (this) {
@ -109,7 +109,7 @@ fun AllContentTab.filtersForSubscribe(
if (limitedObjectIds.isNotEmpty()) {
add(buildLimitedObjectIdsFilter(limitedObjectIds = limitedObjectIds))
}
if (activeMode == AllContentMode.Unlinked) {
if (activeMode == UiTitleState.OnlyUnlinked) {
addAll(buildUnlinkedObjectFilter())
}
}
@ -133,7 +133,6 @@ fun AllContentTab.filtersForSearch(
AllContentTab.BOOKMARKS -> {
val filters = buildList {
addAll(buildDeletedFilter())
add(buildLayoutFilter(layouts = allContentTabLayouts.getValue(tab)))
add(buildSpaceIdFilter(spaces))
if (tab == AllContentTab.PAGES) {
add(buildTemplateFilter())

View file

@ -15,24 +15,20 @@ import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.feature_allcontent.models.UiContentItem
import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode
import com.anytypeio.anytype.feature_allcontent.models.AllContentMode
import com.anytypeio.anytype.feature_allcontent.models.AllContentSort
import com.anytypeio.anytype.feature_allcontent.models.AllContentTab
import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
import com.anytypeio.anytype.feature_allcontent.models.UiContentState
import com.anytypeio.anytype.feature_allcontent.models.MenuButtonViewState
import com.anytypeio.anytype.feature_allcontent.models.MenuSortsItem
import com.anytypeio.anytype.feature_allcontent.models.UiContentItem
import com.anytypeio.anytype.feature_allcontent.models.UiContentState
import com.anytypeio.anytype.feature_allcontent.models.UiMenuState
import com.anytypeio.anytype.feature_allcontent.models.UiTabsState
import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
import com.anytypeio.anytype.feature_allcontent.models.createSubscriptionParams
import com.anytypeio.anytype.feature_allcontent.models.filtersForSearch
import com.anytypeio.anytype.feature_allcontent.models.mapRelationKeyToSort
import com.anytypeio.anytype.feature_allcontent.models.toUiContentItems
import com.anytypeio.anytype.feature_allcontent.models.view
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.home.navigation
@ -43,13 +39,9 @@ import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@ -57,11 +49,11 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import timber.log.Timber
@ -75,7 +67,6 @@ import timber.log.Timber
class AllContentViewModel(
private val vmParams: VmParams,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val storeOfRelations: StoreOfRelations,
private val urlBuilder: UrlBuilder,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
@ -86,12 +77,20 @@ class AllContentViewModel(
private val localeProvider: LocaleProvider
) : ViewModel() {
private val _limitedObjectIds: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
private val searchResultIds = MutableStateFlow<List<Id>>(emptyList())
private val sortState = MutableStateFlow<AllContentSort>(DEFAULT_INITIAL_SORT)
private val _tabsState = MutableStateFlow<AllContentTab>(DEFAULT_INITIAL_TAB)
private val _modeState = MutableStateFlow<AllContentMode>(DEFAULT_INITIAL_MODE)
private val _sortState = MutableStateFlow<AllContentSort>(DEFAULT_INITIAL_SORT)
private val _limitState = MutableStateFlow(DEFAULT_SEARCH_LIMIT)
val uiTitleState = MutableStateFlow<UiTitleState>(UiTitleState.Hidden)
val uiTabsState = MutableStateFlow<UiTabsState>(UiTabsState.Hidden)
val uiMenuState = MutableStateFlow<UiMenuState>(UiMenuState.Hidden)
val uiItemsState = MutableStateFlow<List<UiContentItem>>(emptyList())
val uiContentState = MutableStateFlow<UiContentState>(UiContentState.Idle())
val commands = MutableSharedFlow<Command>()
/**
* Search query
*/
private val userInput = MutableStateFlow(DEFAULT_QUERY)
@OptIn(FlowPreview::class)
@ -101,58 +100,30 @@ class AllContentViewModel(
emitAll(userInput.drop(1).debounce(DEFAULT_DEBOUNCE_DURATION).distinctUntilChanged())
}
private val _uiTitleState = MutableStateFlow<UiTitleState>(UiTitleState.Hidden)
val uiTitleState: StateFlow<UiTitleState> = _uiTitleState.asStateFlow()
/**
* Paging and subscription limit. If true, we can paginate after reaching bottom items.
* Could be true only after the first subscription results (if results size == limit)
*/
val canPaginate = MutableStateFlow(false)
private var itemsLimit = DEFAULT_SEARCH_LIMIT
private val limitUpdateTrigger = MutableStateFlow(0)
private val _uiMenuButtonState =
MutableStateFlow<MenuButtonViewState>(MenuButtonViewState.Hidden)
val uiMenuButtonState: StateFlow<MenuButtonViewState> = _uiMenuButtonState.asStateFlow()
private val _uiTabsState = MutableStateFlow<UiTabsState>(UiTabsState.Hidden)
val uiTabsState: StateFlow<UiTabsState> = _uiTabsState.asStateFlow()
private val _uiState = MutableStateFlow<UiContentState>(UiContentState.Hidden)
val uiState: StateFlow<UiContentState> = _uiState.asStateFlow()
private val _uiMenu = MutableStateFlow(UiMenuState.empty())
val uiMenu: StateFlow<UiMenuState> = _uiMenu.asStateFlow()
private val _commands = MutableSharedFlow<Command>()
val commands: SharedFlow<Command> = _commands
private var shouldScrollToTopItems = false
init {
Timber.d("AllContentViewModel init, spaceId:[${vmParams.spaceId.id}]")
setupInitialStateParams()
proceedWithUiTitleStateSetup()
proceedWithUiTabsStateSetup()
proceedWithUiStateSetup()
proceedWithSearchStateSetup()
proceedWithMenuSetup()
}
private fun proceedWithUiTitleStateSetup() {
viewModelScope.launch {
_modeState.collectLatest { result ->
Timber.d("New mode: [$result]")
_uiTitleState.value = result.view()
_uiMenuButtonState.value = MenuButtonViewState.Visible
}
}
}
private fun proceedWithUiTabsStateSetup() {
viewModelScope.launch {
_tabsState.collectLatest { result ->
Timber.d("New tab: [$result]")
_uiTabsState.value = UiTabsState.Default(
tabs = AllContentTab.entries,
selectedTab = result
)
}
}
setupUiStateFlow()
setupSearchStateFlow()
setupMenuFlow()
}
private fun setupInitialStateParams() {
uiTitleState.value = UiTitleState.AllContent
uiTabsState.value = UiTabsState.Default(
tabs = AllContentTab.entries,
selectedTab = DEFAULT_INITIAL_TAB
)
viewModelScope.launch {
if (vmParams.useHistory) {
runCatching {
@ -160,7 +131,7 @@ class AllContentViewModel(
RestoreAllContentState.Params(vmParams.spaceId)
)
if (!initialParams.activeSort.isNullOrEmpty()) {
_sortState.value = initialParams.activeSort.mapRelationKeyToSort()
sortState.value = initialParams.activeSort.mapRelationKeyToSort()
}
}.onFailure { e ->
Timber.e(e, "Error restoring state")
@ -169,98 +140,101 @@ class AllContentViewModel(
}
}
private fun proceedWithSearchStateSetup() {
private fun setupSearchStateFlow() {
viewModelScope.launch {
searchQuery.collectLatest { query ->
Timber.d("New query: [$query]")
if (query.isBlank()) {
_limitedObjectIds.value = emptyList()
searchResultIds.value = emptyList()
} else {
val searchParams = createSearchParams(
activeTab = _tabsState.value,
activeQuery = query
)
searchObjects(searchParams).process(
success = { searchResults ->
Timber.d("Search objects by query:[$query], size: : ${searchResults.size}")
_limitedObjectIds.value = searchResults.map { it.id }
},
failure = {
Timber.e(it, "Error searching objects by query")
}
)
val activeTab = uiTabsState.value
if (activeTab is UiTabsState.Default) {
resetLimit()
val searchParams = createSearchParams(
activeTab = activeTab.selectedTab,
activeQuery = query
)
searchObjects(searchParams).process(
success = { searchResults ->
Timber.d("Search objects by query:[$query], size: : ${searchResults.size}")
searchResultIds.value = searchResults.map { it.id }
},
failure = {
Timber.e(it, "Error searching objects by query")
}
)
} else {
Timber.w("Unexpected tabs state: $activeTab")
}
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun proceedWithUiStateSetup() {
private fun setupUiStateFlow() {
viewModelScope.launch {
combine(
_modeState,
_tabsState,
_sortState,
_limitedObjectIds,
_limitState
) { mode, tab, sort, limitedObjectIds, limit ->
Result(mode, tab, sort, limitedObjectIds, limit)
uiTitleState,
uiTabsState.filterIsInstance<UiTabsState.Default>(),
sortState,
searchResultIds,
limitUpdateTrigger
) { mode, tab, sort, limitedObjectIds, _ ->
Result(mode, tab, sort, limitedObjectIds)
}
.flatMapLatest { currentState ->
Timber.d("AllContentNewState:$currentState, restart subscription")
Timber.d("New params:$currentState, restart subscription")
loadData(currentState)
}.collect {
_uiState.value = it
uiItemsState.value = it
}
}
}
fun subscriptionId() = "all_content_subscription_${vmParams.spaceId.id}"
private fun subscriptionId() = "all_content_subscription_${vmParams.spaceId.id}"
private fun loadData(
result: Result
): Flow<UiContentState> = flow {
val loadingStartTime = System.currentTimeMillis()
): Flow<List<UiContentItem>> = flow {
emit(UiContentState.Loading)
if (itemsLimit == DEFAULT_SEARCH_LIMIT) {
uiContentState.value = UiContentState.InitLoading
} else {
uiContentState.value = UiContentState.Paging
}
val searchParams = createSubscriptionParams(
activeTab = result.tab,
activeTab = result.tab.selectedTab,
activeSort = result.sort,
limitedObjectIds = result.limitedObjectIds,
limit = result.limit,
limit = itemsLimit,
subscriptionId = subscriptionId(),
spaceId = vmParams.spaceId.id,
activeMode = result.mode
)
val dataFlow = storelessSubscriptionContainer.subscribe(searchParams)
.map { objWrappers ->
emitAll(
dataFlow.map { objWrappers ->
canPaginate.value = objWrappers.size == itemsLimit
val items = mapToUiContentItems(
objectWrappers = objWrappers,
activeSort = result.sort
)
UiContentState.Content(items = items)
}
.catch { e ->
emit(
UiContentState.Error(
message = e.message ?: "Error loading objects by subscription"
)
)
}
var isFirstEmission = true
emitAll(
dataFlow.onEach {
if (isFirstEmission) {
val elapsedTime = System.currentTimeMillis() - loadingStartTime
if (elapsedTime < DEFAULT_LOADING_DELAY) {
delay(DEFAULT_LOADING_DELAY - elapsedTime)
uiContentState.value = if (items.isEmpty()) {
UiContentState.Empty
} else {
UiContentState.Idle(scrollToTop = shouldScrollToTopItems).also {
shouldScrollToTopItems = false
}
isFirstEmission = false
}
items
}.catch { e ->
uiContentState.value = UiContentState.Error(
message = e.message ?: "An error occurred while loading data."
)
emit(emptyList())
}
)
}
@ -274,28 +248,31 @@ class AllContentViewModel(
urlBuilder = urlBuilder,
objectTypes = storeOfObjectTypes.getAll()
)
return if (activeSort.canGroupByDate) {
groupItemsByDate(
items = items,
activeSort = activeSort
)
} else {
items
return when (activeSort) {
is AllContentSort.ByDateCreated -> {
groupItemsByDate(items = items, isSortByDateCreated = true)
}
is AllContentSort.ByDateUpdated -> {
groupItemsByDate(items = items, isSortByDateCreated = false)
}
is AllContentSort.ByName -> {
items
}
}
}
private fun groupItemsByDate(
items: List<UiContentItem.Item>,
activeSort: AllContentSort
isSortByDateCreated: Boolean
): List<UiContentItem> {
val groupedItems = mutableListOf<UiContentItem>()
var currentGroupKey: String? = null
for (item in items) {
val timestamp = when (activeSort) {
is AllContentSort.ByDateCreated -> item.createdDate
is AllContentSort.ByDateUpdated -> item.lastModifiedDate
is AllContentSort.ByName -> 0L
val timestamp = if (isSortByDateCreated) {
item.createdDate
} else {
item.lastModifiedDate
}
val (groupKey, group) = getDateGroup(timestamp)
@ -347,7 +324,6 @@ class AllContentViewModel(
}
}
// Function to create search parameters
private fun createSearchParams(
activeTab: AllContentTab,
activeQuery: String,
@ -362,25 +338,17 @@ class AllContentViewModel(
)
}
// Function to get the menu mode based on the active mode
private fun getMenuMode(mode: AllContentMode): AllContentMenuMode {
return when (mode) {
AllContentMode.AllContent -> AllContentMenuMode.AllContent(isSelected = true)
AllContentMode.Unlinked -> AllContentMenuMode.Unlinked(isSelected = true)
}
}
private fun proceedWithMenuSetup() {
private fun setupMenuFlow() {
viewModelScope.launch {
combine(
_modeState,
_sortState
uiTitleState,
sortState
) { mode, sort ->
mode to sort
}.collectLatest { (mode, sort) ->
val uiMode = listOf(
AllContentMenuMode.AllContent(isSelected = mode == AllContentMode.AllContent),
AllContentMenuMode.Unlinked(isSelected = mode == AllContentMode.Unlinked)
AllContentMenuMode.AllContent(isSelected = mode == UiTitleState.AllContent),
AllContentMenuMode.Unlinked(isSelected = mode == UiTitleState.OnlyUnlinked)
)
val container = MenuSortsItem.Container(sort = sort)
val uiSorts = listOf(
@ -406,7 +374,7 @@ class AllContentViewModel(
isSelected = sort.sortType == DVSortType.DESC
)
)
_uiMenu.value = UiMenuState(
uiMenuState.value = UiMenuState.Visible(
mode = uiMode,
container = container,
sorts = uiSorts,
@ -420,24 +388,34 @@ class AllContentViewModel(
Timber.d("onTabClicked: $tab")
if (tab == AllContentTab.TYPES) {
viewModelScope.launch {
_commands.emit(Command.SendToast("Not implemented yet"))
commands.emit(Command.SendToast("Not implemented yet"))
}
return
}
_tabsState.value = tab
shouldScrollToTopItems = true
resetLimit()
uiItemsState.value = emptyList()
uiTabsState.value = UiTabsState.Default(
tabs = AllContentTab.entries,
selectedTab = tab
)
}
fun onAllContentModeClicked(mode: AllContentMenuMode) {
Timber.d("onAllContentModeClicked: $mode")
_modeState.value = when (mode) {
is AllContentMenuMode.AllContent -> AllContentMode.AllContent
is AllContentMenuMode.Unlinked -> AllContentMode.Unlinked
shouldScrollToTopItems = true
uiItemsState.value = emptyList()
uiTitleState.value = when (mode) {
is AllContentMenuMode.AllContent -> UiTitleState.AllContent
is AllContentMenuMode.Unlinked -> UiTitleState.OnlyUnlinked
}
}
fun onSortClicked(sort: AllContentSort) {
Timber.d("onSortClicked: $sort")
_sortState.value = sort
shouldScrollToTopItems = true
uiItemsState.value = emptyList()
sortState.value = sort
proceedWithSortSaving(sort)
}
@ -463,14 +441,9 @@ class AllContentViewModel(
userInput.value = filter
}
fun onLimitUpdated(limit: Int) {
Timber.d("onLimitUpdated: $limit")
_limitState.value = limit
}
fun onViewBinClicked() {
viewModelScope.launch {
_commands.emit(Command.NavigateToBin(vmParams.spaceId.id))
commands.emit(Command.NavigateToBin(vmParams.spaceId.id))
}
}
@ -483,7 +456,7 @@ class AllContentViewModel(
space = vmParams.spaceId.id
)) {
is OpenObjectNavigation.OpenDataView -> {
_commands.emit(
commands.emit(
Command.NavigateToSetOrCollection(
id = navigation.target,
space = navigation.space
@ -492,7 +465,7 @@ class AllContentViewModel(
}
is OpenObjectNavigation.OpenEditor -> {
_commands.emit(
commands.emit(
Command.NavigateToEditor(
id = navigation.target,
space = navigation.space
@ -501,7 +474,7 @@ class AllContentViewModel(
}
is OpenObjectNavigation.UnexpectedLayoutError -> {
_commands.emit(Command.SendToast("Unexpected layout: ${navigation.layout}"))
commands.emit(Command.SendToast("Unexpected layout: ${navigation.layout}"))
}
}
}
@ -514,17 +487,38 @@ class AllContentViewModel(
}
}
/**
* Updates the limit for the number of items fetched and triggers data reload.
*/
fun updateLimit() {
Timber.d("Update limit, canPaginate: ${canPaginate.value} uiContentState: ${uiContentState.value}")
if (canPaginate.value && uiContentState.value is UiContentState.Idle) {
itemsLimit += DEFAULT_SEARCH_LIMIT
limitUpdateTrigger.value++
}
}
override fun onCleared() {
super.onCleared()
uiItemsState.value = emptyList()
resetLimit()
}
private fun resetLimit() {
Timber.d("Reset limit")
itemsLimit = DEFAULT_SEARCH_LIMIT
}
data class VmParams(
val spaceId: SpaceId,
val useHistory: Boolean = true
)
internal data class Result(
val mode: AllContentMode,
val tab: AllContentTab,
val mode: UiTitleState,
val tab: UiTabsState.Default,
val sort: AllContentSort,
val limitedObjectIds: List<String>,
val limit: Int
val limitedObjectIds: List<String>
)
sealed class Command {
@ -536,13 +530,11 @@ class AllContentViewModel(
companion object {
const val DEFAULT_DEBOUNCE_DURATION = 300L
const val DEFAULT_LOADING_DELAY = 250L
//INITIAL STATE
const val DEFAULT_SEARCH_LIMIT = 50
const val DEFAULT_SEARCH_LIMIT = 100
val DEFAULT_INITIAL_TAB = AllContentTab.PAGES
val DEFAULT_INITIAL_SORT = AllContentSort.ByName()
val DEFAULT_INITIAL_MODE = AllContentMode.AllContent
val DEFAULT_QUERY = ""
}
}

View file

@ -9,17 +9,14 @@ import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel.VmParams
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import javax.inject.Inject
import javax.inject.Named
class AllContentViewModelFactory @Inject constructor(
private val vmParams: VmParams,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val storeOfRelations: StoreOfRelations,
private val urlBuilder: UrlBuilder,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
@ -34,7 +31,6 @@ class AllContentViewModelFactory @Inject constructor(
AllContentViewModel(
vmParams = vmParams,
storeOfObjectTypes = storeOfObjectTypes,
storeOfRelations = storeOfRelations,
urlBuilder = urlBuilder,
analytics = analytics,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,

View file

@ -39,7 +39,7 @@ import com.anytypeio.anytype.feature_allcontent.models.UiMenuState
@Composable
fun AllContentMenu(
uiMenuState: UiMenuState,
uiMenuState: UiMenuState.Visible,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
onBinClick: () -> Unit
@ -233,7 +233,7 @@ private fun DVSortType.title(sort: AllContentSort): String = when (this) {
@Composable
fun AllContentMenuPreview() {
AllContentMenu(
uiMenuState = UiMenuState(
uiMenuState = UiMenuState.Visible(
mode = listOf(
AllContentMenuMode.AllContent(isSelected = true),
AllContentMenuMode.Unlinked(isSelected = false)

View file

@ -20,12 +20,17 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
@ -69,32 +74,54 @@ import com.anytypeio.anytype.feature_allcontent.R
import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode
import com.anytypeio.anytype.feature_allcontent.models.AllContentSort
import com.anytypeio.anytype.feature_allcontent.models.AllContentTab
import com.anytypeio.anytype.feature_allcontent.models.UiContentState
import com.anytypeio.anytype.feature_allcontent.models.MenuButtonViewState
import com.anytypeio.anytype.feature_allcontent.models.UiContentItem
import com.anytypeio.anytype.feature_allcontent.models.UiContentState
import com.anytypeio.anytype.feature_allcontent.models.UiMenuState
import com.anytypeio.anytype.feature_allcontent.models.UiTabsState
import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import kotlinx.coroutines.launch
@Composable
fun AllContentWrapperScreen(
uiTitleState: UiTitleState,
uiTabsState: UiTabsState,
uiMenuButtonViewState: MenuButtonViewState,
uiMenuState: UiMenuState,
uiState: UiContentState,
uiItemsState: List<UiContentItem>,
onTabClick: (AllContentTab) -> Unit,
onQueryChanged: (String) -> Unit,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
onItemClicked: (UiContentItem.Item) -> Unit,
onBinClick: () -> Unit
onBinClick: () -> Unit,
canPaginate: Boolean,
onUpdateLimitSearch: () -> Unit,
uiContentState: UiContentState
) {
val lazyListState = rememberLazyListState()
val canPaginateState = remember { mutableStateOf(false) }
LaunchedEffect(key1 = canPaginate) {
canPaginateState.value = canPaginate
}
val shouldStartPaging = remember {
derivedStateOf {
canPaginateState.value && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: -9) >= (lazyListState.layoutInfo.totalItemsCount - 2)
}
}
LaunchedEffect(key1 = shouldStartPaging.value) {
if (shouldStartPaging.value && uiContentState is UiContentState.Idle) {
onUpdateLimitSearch()
}
}
AllContentMainScreen(
uiTitleState = uiTitleState,
uiTabsState = uiTabsState,
uiMenuButtonViewState = uiMenuButtonViewState,
onTabClick = onTabClick,
onQueryChanged = onQueryChanged,
uiMenuState = uiMenuState,
@ -102,23 +129,27 @@ fun AllContentWrapperScreen(
onSortClick = onSortClick,
onItemClicked = onItemClicked,
onBinClick = onBinClick,
uiState = uiState
uiItemsState = uiItemsState,
lazyListState = lazyListState,
uiContentState = uiContentState
)
}
@Composable
fun AllContentMainScreen(
uiState: UiContentState,
uiItemsState: List<UiContentItem>,
uiTitleState: UiTitleState,
uiTabsState: UiTabsState,
uiMenuButtonViewState: MenuButtonViewState,
uiMenuState: UiMenuState,
onTabClick: (AllContentTab) -> Unit,
onQueryChanged: (String) -> Unit,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
onItemClicked: (UiContentItem.Item) -> Unit,
onBinClick: () -> Unit
onBinClick: () -> Unit,
lazyListState: LazyListState,
uiContentState: UiContentState
) {
val modifier = Modifier
.background(color = colorResource(id = R.color.background_primary))
@ -141,7 +172,6 @@ fun AllContentMainScreen(
if (uiTitleState !is UiTitleState.Hidden) {
AllContentTopBarContainer(
titleState = uiTitleState,
menuButtonState = uiMenuButtonViewState,
uiMenuState = uiMenuState,
onSortClick = onSortClick,
onModeClick = onModeClick,
@ -175,34 +205,37 @@ fun AllContentMainScreen(
.fillMaxSize()
.padding(paddingValues)
when (uiState) {
is UiContentState.Content -> {
if (uiState.items.isEmpty()) {
Box(modifier = contentModifier, contentAlignment = Alignment.Center) {
EmptyState(isSearchEmpty = isSearchEmpty)
Box(
modifier = contentModifier,
contentAlignment = Alignment.Center
) {
when {
uiItemsState.isEmpty() -> {
when (uiContentState) {
is UiContentState.Error -> {
ErrorState(uiContentState.message)
}
is UiContentState.Idle -> {
// Do nothing.
}
UiContentState.InitLoading -> {
LoadingState()
}
UiContentState.Paging -> {}
UiContentState.Empty -> {
EmptyState(isSearchEmpty = isSearchEmpty)
}
}
} else {
}
else -> {
ContentItems(
modifier = contentModifier,
items = uiState.items,
onItemClicked = onItemClicked
uiItemsState = uiItemsState,
onItemClicked = onItemClicked,
uiContentState = uiContentState,
lazyListState = lazyListState
)
}
}
is UiContentState.Error -> {
Box(
modifier = contentModifier,
contentAlignment = Alignment.Center
) {
ErrorState(uiState.message)
}
}
UiContentState.Hidden -> {}
UiContentState.Loading -> {
Box(modifier = contentModifier) {
LoadingState()
}
}
}
}
)
@ -210,27 +243,34 @@ fun AllContentMainScreen(
@Composable
private fun ContentItems(
modifier: Modifier,
items: List<UiContentItem>,
onItemClicked: (UiContentItem.Item) -> Unit
uiItemsState: List<UiContentItem>,
onItemClicked: (UiContentItem.Item) -> Unit,
uiContentState: UiContentState,
lazyListState: LazyListState
) {
LazyColumn(modifier = modifier) {
val scope = rememberCoroutineScope()
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState
) {
items(
count = items.size,
key = { index -> items[index].id },
count = uiItemsState.size,
key = { index -> uiItemsState[index].id },
contentType = { index ->
when (items[index]) {
when (uiItemsState[index]) {
is UiContentItem.Group -> "group"
is UiContentItem.Item -> "item"
}
}
) { index ->
when (val item = items[index]) {
when (val item = uiItemsState[index]) {
is UiContentItem.Group -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
.height(52.dp)
.animateItem(),
contentAlignment = Alignment.BottomStart
) {
Text(
@ -258,6 +298,28 @@ private fun ContentItems(
}
}
}
if (uiContentState is UiContentState.Paging) {
item {
Box(
modifier = Modifier
.fillParentMaxWidth()
.height(52.dp),
contentAlignment = Alignment.Center
) {
LoadingState()
}
}
}
}
LaunchedEffect(key1 = uiContentState) {
if (uiContentState is UiContentState.Idle) {
if (uiContentState.scrollToTop) {
scope.launch {
lazyListState.scrollToItem(0)
}
}
}
}
}
@ -283,6 +345,25 @@ fun PreviewLoadingState() {
}
}
@DefaultPreviews
@Composable
fun PreviewMainScreen() {
AllContentMainScreen(
uiItemsState = emptyList(),
uiTitleState = UiTitleState.AllContent,
uiTabsState = UiTabsState.Default(tabs = listOf(AllContentTab.PAGES, AllContentTab.TYPES, AllContentTab.LISTS), selectedTab = AllContentTab.LISTS),
uiMenuState = UiMenuState.Hidden,
onTabClick = {},
onQueryChanged = {},
onModeClick = {},
onSortClick = {},
onItemClicked = {},
onBinClick = {},
lazyListState = rememberLazyListState(),
uiContentState = UiContentState.Error("Error message")
)
}
@Composable
private fun Item(
modifier: Modifier,

View file

@ -63,7 +63,6 @@ import com.anytypeio.anytype.feature_allcontent.R
import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode
import com.anytypeio.anytype.feature_allcontent.models.AllContentSort
import com.anytypeio.anytype.feature_allcontent.models.AllContentTab
import com.anytypeio.anytype.feature_allcontent.models.MenuButtonViewState
import com.anytypeio.anytype.feature_allcontent.models.MenuSortsItem
import com.anytypeio.anytype.feature_allcontent.models.UiMenuState
import com.anytypeio.anytype.feature_allcontent.models.UiTabsState
@ -74,7 +73,6 @@ import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
@Composable
fun AllContentTopBarContainer(
titleState: UiTitleState,
menuButtonState: MenuButtonViewState,
uiMenuState: UiMenuState,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
@ -88,29 +86,30 @@ fun AllContentTopBarContainer(
title = { AllContentTitle(state = titleState) },
actions = {
AllContentMenuButton(
state = menuButtonState,
onClick = { isMenuExpanded = true }
)
DropdownMenu(
modifier = Modifier.width(252.dp),
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
shape = RoundedCornerShape(size = 16.dp),
containerColor = colorResource(id = R.color.background_primary),
shadowElevation = 5.dp
) {
AllContentMenu(
uiMenuState = uiMenuState,
onModeClick = {
onModeClick(it)
isMenuExpanded = false
},
onSortClick = {
onSortClick(it)
isMenuExpanded = false
},
onBinClick = onBinClick
)
if (uiMenuState is UiMenuState.Visible) {
DropdownMenu(
modifier = Modifier.width(252.dp),
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
shape = RoundedCornerShape(size = 16.dp),
containerColor = colorResource(id = R.color.background_primary),
shadowElevation = 5.dp
) {
AllContentMenu(
uiMenuState = uiMenuState,
onModeClick = {
onModeClick(it)
isMenuExpanded = false
},
onSortClick = {
onSortClick(it)
isMenuExpanded = false
},
onBinClick = onBinClick
)
}
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
@ -124,8 +123,7 @@ fun AllContentTopBarContainer(
private fun AllContentTopBarContainerPreview() {
AllContentTopBarContainer(
titleState = UiTitleState.OnlyUnlinked,
menuButtonState = MenuButtonViewState.Visible,
uiMenuState = UiMenuState(
uiMenuState = UiMenuState.Visible(
mode = listOf(
AllContentMenuMode.AllContent(isSelected = true),
AllContentMenuMode.Unlinked()
@ -188,21 +186,16 @@ fun AllContentTitle(state: UiTitleState) {
//region AllContentMenuButton
@Composable
fun AllContentMenuButton(state: MenuButtonViewState, onClick: () -> Unit) {
when (state) {
MenuButtonViewState.Hidden -> return
MenuButtonViewState.Visible -> {
Image(
modifier = Modifier
.padding(end = 12.dp)
.size(32.dp)
.bouncingClickable { onClick() },
painter = painterResource(id = R.drawable.ic_space_list_dots),
contentDescription = "Menu icon",
contentScale = ContentScale.Inside
)
}
}
fun AllContentMenuButton(onClick: () -> Unit) {
Image(
modifier = Modifier
.padding(end = 12.dp)
.size(32.dp)
.bouncingClickable { onClick() },
painter = painterResource(id = R.drawable.ic_space_list_dots),
contentDescription = "Menu icon",
contentScale = ContentScale.Inside
)
}
//endregion