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

DROID-2836 All content | epic (#1608)

This commit is contained in:
Konstantin Ivanov 2024-09-30 00:00:43 +02:00 committed by GitHub
parent a1dcdda9d6
commit 7c03809e18
Signed by: github
GPG key ID: B5690EEEBB952194
29 changed files with 2669 additions and 11 deletions

View file

@ -0,0 +1,60 @@
plugins {
id "com.android.library"
id "kotlin-android"
alias(libs.plugins.compose.compiler)
}
android {
defaultConfig {
buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true"
buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true"
}
buildFeatures {
compose true
}
namespace 'com.anytypeio.anytype.feature_allcontent'
}
dependencies {
implementation project(':domain')
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
implementation project(':localization')
implementation project(':presentation')
implementation project(':library-emojifier')
compileOnly libs.javaxInject
implementation libs.lifecycleViewModel
implementation libs.lifecycleRuntime
implementation libs.appcompat
implementation libs.compose
implementation libs.composeFoundation
implementation libs.composeToolingPreview
implementation libs.composeMaterial3
implementation libs.composeMaterial
implementation libs.navigationCompose
debugImplementation libs.composeTooling
implementation libs.timber
testImplementation project(':test:android-utils')
testImplementation project(':test:utils')
testImplementation project(":test:core-models-stub")
testImplementation libs.junit
testImplementation libs.kotlinTest
testImplementation libs.robolectric
testImplementation libs.androidXTestCore
testImplementation libs.mockitoKotlin
testImplementation libs.coroutineTesting
testImplementation libs.timberJUnit
testImplementation libs.turbine
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest/>

View file

@ -0,0 +1,256 @@
package com.anytypeio.anytype.feature_allcontent.models
import androidx.compose.runtime.Immutable
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.DateParser
import com.anytypeio.anytype.core_models.primitives.RelationKey
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel.Companion.DEFAULT_INITIAL_SORT
import com.anytypeio.anytype.presentation.objects.ObjectIcon
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, RELATIONS
}
sealed class AllContentMode {
data object AllContent : AllContentMode()
data object Unlinked : AllContentMode()
}
sealed class AllContentMenuMode {
abstract val isSelected: Boolean
data class AllContent(
override val isSelected: Boolean = false
) : AllContentMenuMode()
data class Unlinked(
override val isSelected: Boolean = false
) : AllContentMenuMode()
}
sealed class AllContentSort {
abstract val relationKey: RelationKey
abstract val sortType: DVSortType
abstract val canGroupByDate: Boolean
abstract val isSelected: Boolean
data class ByName(
override val relationKey: RelationKey = RelationKey(Relations.NAME),
override val sortType: DVSortType = DVSortType.ASC,
override val canGroupByDate: Boolean = false,
override val isSelected: Boolean = false
) : AllContentSort()
data class ByDateUpdated(
override val relationKey: RelationKey = RelationKey(Relations.LAST_MODIFIED_DATE),
override val sortType: DVSortType = DVSortType.DESC,
override val canGroupByDate: Boolean = true,
override val isSelected: Boolean = false
) : AllContentSort()
data class ByDateCreated(
override val relationKey: RelationKey = RelationKey(Relations.CREATED_DATE),
override val sortType: DVSortType = DVSortType.DESC,
override val canGroupByDate: Boolean = true,
override val isSelected: Boolean = false
) : AllContentSort()
}
//endregion
//region VIEW STATES
//TITLE
sealed class UiTitleState {
data object Hidden : UiTitleState()
data object AllContent : UiTitleState()
data object OnlyUnlinked : UiTitleState()
}
//MENU BUTTON
sealed class MenuButtonViewState {
data object Hidden : MenuButtonViewState()
data object Visible : MenuButtonViewState()
}
// TABS
@Immutable
sealed class UiTabsState {
data object Hidden : UiTabsState()
@Immutable
data class Default(
val tabs: List<AllContentTab>,
val selectedTab: AllContentTab
) : UiTabsState()
}
// CONTENT
sealed class UiContentState {
data object Hidden : UiContentState()
data object Loading : UiContentState()
data class Error(
val message: String,
) : UiContentState()
@Immutable
data class Content(
val items: List<UiContentItem>,
) : UiContentState()
}
// ITEMS
sealed class UiContentItem {
abstract val id: String
sealed class Group : UiContentItem() {
data class Today(override val id: String = TODAY_ID) : Group()
data class Yesterday(override val id: String = YESTERDAY_ID) : Group()
data class Previous7Days(override val id: String = PREVIOUS_7_DAYS_ID) : Group()
data class Previous14Days(override val id: String = PREVIOUS_14_DAYS_ID) : Group()
data class Month(override val id: String, val title: String) : Group()
data class MonthAndYear(override val id: String, val title: String) : Group()
}
data class Item(
override val id: String,
val name: String,
val space: SpaceId,
val type: String? = null,
val typeName: String? = null,
val description: String? = null,
val layout: ObjectType.Layout? = null,
val icon: ObjectIcon = ObjectIcon.None,
val lastModifiedDate: Long = 0L,
val createdDate: Long = 0L,
) : UiContentItem()
companion object {
const val TODAY_ID = "TodayId"
const val YESTERDAY_ID = "YesterdayId"
const val PREVIOUS_7_DAYS_ID = "Previous7DaysId"
const val PREVIOUS_14_DAYS_ID = "Previous14DaysId"
}
}
// MENU
@Immutable
data class UiMenuState(
val mode: List<AllContentMenuMode>,
val container: MenuSortsItem.Container,
val sorts: List<MenuSortsItem.Sort>,
val types: List<MenuSortsItem.SortType>
) {
companion object {
fun empty(): UiMenuState {
return UiMenuState(
mode = emptyList(),
container = MenuSortsItem.Container(AllContentSort.ByName()),
sorts = emptyList(),
types = emptyList()
)
}
}
}
sealed class MenuSortsItem {
data class Container(val sort: AllContentSort) : MenuSortsItem()
data class Sort(val sort: AllContentSort) : MenuSortsItem()
data object Spacer : MenuSortsItem()
data class SortType(
val sort: AllContentSort,
val sortType: DVSortType,
val isSelected: Boolean
) : 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()
Relations.LAST_OPENED_DATE -> AllContentSort.ByDateUpdated()
else -> DEFAULT_INITIAL_SORT
}
}
fun List<ObjectWrapper.Basic>.toUiContentItems(
space: SpaceId,
urlBuilder: UrlBuilder,
objectTypes: List<ObjectWrapper.Type>
): List<UiContentItem.Item> {
return map { it.toAllContentItem(space, urlBuilder, objectTypes) }
}
fun ObjectWrapper.Basic.toAllContentItem(
space: SpaceId,
urlBuilder: UrlBuilder,
objectTypes: List<ObjectWrapper.Type>
): UiContentItem.Item {
val obj = this
val typeUrl = obj.getProperType()
val isProfile = typeUrl == MarketplaceObjectTypeIds.PROFILE
val layout = layout ?: ObjectType.Layout.BASIC
return UiContentItem.Item(
id = obj.id,
space = space,
name = obj.getProperName(),
description = obj.description,
type = typeUrl,
typeName = objectTypes.firstOrNull { type ->
if (isProfile) {
type.uniqueKey == ObjectTypeUniqueKeys.PROFILE
} else {
type.id == typeUrl
}
}?.name,
layout = layout,
icon = ObjectIcon.from(
obj = obj,
layout = layout,
builder = urlBuilder
),
lastModifiedDate = DateParser.parseInMillis(obj.lastModifiedDate) ?: 0L,
createdDate = DateParser.parse(obj.getValue(Relations.CREATED_DATE)) ?: 0L
)
}
//endregion

View file

@ -0,0 +1,229 @@
package com.anytypeio.anytype.feature_allcontent.models
import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVFilterCondition
import com.anytypeio.anytype.core_models.DVSort
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.domain.library.StoreSearchParams
val allContentTabLayouts = mapOf(
AllContentTab.PAGES to listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE
),
AllContentTab.LISTS to listOf(
ObjectType.Layout.SET,
ObjectType.Layout.COLLECTION
),
AllContentTab.FILES to listOf(
ObjectType.Layout.FILE,
ObjectType.Layout.PDF
),
AllContentTab.MEDIA to listOf(
ObjectType.Layout.IMAGE,
ObjectType.Layout.VIDEO,
ObjectType.Layout.AUDIO
),
AllContentTab.BOOKMARKS to listOf(
ObjectType.Layout.BOOKMARK
)
)
// Function to create subscription params
fun createSubscriptionParams(
spaceId: Id,
activeMode: AllContentMode,
activeTab: AllContentTab,
activeSort: AllContentSort,
limitedObjectIds: List<String>,
limit: Int,
subscriptionId: String
): StoreSearchParams {
val (filters, sorts) = activeTab.filtersForSubscribe(
spaces = listOf(spaceId),
activeSort = activeSort,
limitedObjectIds = limitedObjectIds,
activeMode = activeMode
)
return StoreSearchParams(
filters = filters,
sorts = sorts,
keys = listOf(
Relations.ID,
Relations.SPACE_ID,
Relations.TARGET_SPACE_ID,
Relations.UNIQUE_KEY,
Relations.NAME,
Relations.ICON_IMAGE,
Relations.ICON_EMOJI,
Relations.ICON_OPTION,
Relations.TYPE,
Relations.LAYOUT,
Relations.IS_ARCHIVED,
Relations.IS_DELETED,
Relations.IS_HIDDEN,
Relations.SNIPPET,
Relations.DONE,
Relations.IDENTITY_PROFILE_LINK,
Relations.RESTRICTIONS,
Relations.SIZE_IN_BYTES,
Relations.FILE_MIME_TYPE,
Relations.FILE_EXT,
Relations.LAST_OPENED_DATE,
Relations.LAST_MODIFIED_DATE,
Relations.CREATED_DATE,
Relations.LINKS,
Relations.BACKLINKS
),
limit = limit,
subscription = subscriptionId
)
}
fun AllContentTab.filtersForSubscribe(
spaces: List<Id>,
activeSort: AllContentSort,
limitedObjectIds: List<Id>,
activeMode: AllContentMode
): Pair<List<DVFilter>, List<DVSort>> {
val tab = this
when (this) {
AllContentTab.PAGES,
AllContentTab.LISTS,
AllContentTab.FILES,
AllContentTab.MEDIA,
AllContentTab.BOOKMARKS -> {
val filters = buildList {
addAll(buildDeletedFilter())
add(buildLayoutFilter(layouts = allContentTabLayouts.getValue(tab)))
add(buildSpaceIdFilter(spaces))
if (tab == AllContentTab.PAGES) {
add(buildTemplateFilter())
}
if (limitedObjectIds.isNotEmpty()) {
add(buildLimitedObjectIdsFilter(limitedObjectIds = limitedObjectIds))
}
if (activeMode == AllContentMode.Unlinked) {
addAll(buildUnlinkedObjectFilter())
}
}
val sorts = listOf(activeSort.toDVSort())
return filters to sorts
}
AllContentTab.TYPES -> TODO()
AllContentTab.RELATIONS -> TODO()
}
}
fun AllContentTab.filtersForSearch(
spaces: List<Id>
): List<DVFilter> {
val tab = this
when (this) {
AllContentTab.PAGES,
AllContentTab.LISTS,
AllContentTab.FILES,
AllContentTab.MEDIA,
AllContentTab.BOOKMARKS -> {
val filters = buildList {
addAll(buildDeletedFilter())
add(buildLayoutFilter(layouts = allContentTabLayouts.getValue(tab)))
add(buildSpaceIdFilter(spaces))
if (tab == AllContentTab.PAGES) {
add(buildTemplateFilter())
}
}
return filters
}
AllContentTab.TYPES -> TODO()
AllContentTab.RELATIONS -> TODO()
}
}
private fun buildLayoutFilter(layouts: List<ObjectType.Layout>): DVFilter = DVFilter(
relation = Relations.LAYOUT,
condition = DVFilterCondition.IN,
value = layouts.map { it.code.toDouble() }
)
private fun buildTemplateFilter(): DVFilter = DVFilter(
relation = Relations.TYPE_UNIQUE_KEY,
condition = DVFilterCondition.NOT_EQUAL,
value = ObjectTypeUniqueKeys.TEMPLATE
)
private fun buildSpaceIdFilter(spaces: List<Id>): DVFilter = DVFilter(
relation = Relations.SPACE_ID,
condition = DVFilterCondition.IN,
value = spaces
)
private fun buildUnlinkedObjectFilter(): List<DVFilter> = listOf(
DVFilter(
relation = Relations.LINKS,
condition = DVFilterCondition.EMPTY
),
DVFilter(
relation = Relations.BACKLINKS,
condition = DVFilterCondition.EMPTY
)
)
private fun buildLimitedObjectIdsFilter(limitedObjectIds: List<Id>): DVFilter = DVFilter(
relation = Relations.ID,
condition = DVFilterCondition.IN,
value = limitedObjectIds
)
private fun buildDeletedFilter(): List<DVFilter> {
return listOf(
DVFilter(
relation = Relations.IS_ARCHIVED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_HIDDEN,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_DELETED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
)
)
}
fun AllContentSort.toDVSort(): DVSort {
return when (this) {
is AllContentSort.ByDateCreated -> DVSort(
relationKey = relationKey.key,
type = sortType,
relationFormat = RelationFormat.DATE,
includeTime = true,
)
is AllContentSort.ByDateUpdated -> DVSort(
relationKey = relationKey.key,
type = sortType,
relationFormat = RelationFormat.DATE,
includeTime = true,
)
is AllContentSort.ByName -> DVSort(
relationKey = relationKey.key,
type = sortType,
relationFormat = RelationFormat.LONG_TEXT,
includeTime = false
)
}
}

View file

@ -0,0 +1,523 @@
package com.anytypeio.anytype.feature_allcontent.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.all_content.RestoreAllContentState
import com.anytypeio.anytype.domain.all_content.UpdateAllContentState
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.UiMenuState
import com.anytypeio.anytype.feature_allcontent.models.UiTabsState
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
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import javax.inject.Named
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
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
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
/**
* ViewState: @see [UiContentState]
* Factory: @see [AllContentViewModelFactory]
* Screen: @see [com.anytypeio.anytype.feature_allcontent.ui.AllContentWrapperScreen]
*/
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,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val updateAllContentState: UpdateAllContentState,
private val restoreAllContentState: RestoreAllContentState,
private val searchObjects: SearchObjects,
private val localeProvider: LocaleProvider
) : ViewModel() {
private val _limitedObjectIds: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
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)
private val userInput = MutableStateFlow(DEFAULT_QUERY)
@OptIn(FlowPreview::class)
private val searchQuery = userInput
.take(1)
.onCompletion {
emitAll(userInput.drop(1).debounce(DEFAULT_DEBOUNCE_DURATION).distinctUntilChanged())
}
private val _uiTitleState = MutableStateFlow<UiTitleState>(UiTitleState.Hidden)
val uiTitleState: StateFlow<UiTitleState> = _uiTitleState.asStateFlow()
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
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
)
}
}
}
private fun setupInitialStateParams() {
viewModelScope.launch {
if (vmParams.useHistory) {
runCatching {
val initialParams = restoreAllContentState.run(
RestoreAllContentState.Params(vmParams.spaceId)
)
if (initialParams.activeSort != null) {
_sortState.value = initialParams.activeSort.mapRelationKeyToSort()
}
}.onFailure { e ->
Timber.e(e, "Error restoring state")
}
}
}
}
private fun proceedWithSearchStateSetup() {
viewModelScope.launch {
searchQuery.collectLatest { query ->
Timber.d("New query: [$query]")
if (query.isBlank()) {
_limitedObjectIds.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")
}
)
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun proceedWithUiStateSetup() {
viewModelScope.launch {
combine(
_modeState,
_tabsState,
_sortState,
_limitedObjectIds,
_limitState
) { mode, tab, sort, limitedObjectIds, limit ->
Result(mode, tab, sort, limitedObjectIds, limit)
}
.flatMapLatest { currentState ->
Timber.d("AllContentNewState:$currentState, restart subscription")
loadData(currentState)
}.collect {
_uiState.value = it
}
}
}
fun subscriptionId() = "all_content_subscription_${vmParams.spaceId.id}"
private fun loadData(
result: Result
): Flow<UiContentState> = flow {
val loadingStartTime = System.currentTimeMillis()
emit(UiContentState.Loading)
val searchParams = createSubscriptionParams(
activeTab = result.tab,
activeSort = result.sort,
limitedObjectIds = result.limitedObjectIds,
limit = result.limit,
subscriptionId = subscriptionId(),
spaceId = vmParams.spaceId.id,
activeMode = result.mode
)
val dataFlow = storelessSubscriptionContainer.subscribe(searchParams)
.map { objWrappers ->
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)
}
isFirstEmission = false
}
}
)
}
private suspend fun mapToUiContentItems(
objectWrappers: List<ObjectWrapper.Basic>,
activeSort: AllContentSort
): List<UiContentItem> {
val items = objectWrappers.toUiContentItems(
space = vmParams.spaceId,
urlBuilder = urlBuilder,
objectTypes = storeOfObjectTypes.getAll()
)
return if (activeSort.canGroupByDate) {
groupItemsByDate(
items = items,
activeSort = activeSort
)
} else {
items
}
}
private fun groupItemsByDate(
items: List<UiContentItem.Item>,
activeSort: AllContentSort
): 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 (groupKey, group) = getDateGroup(timestamp)
if (currentGroupKey != groupKey) {
groupedItems.add(group)
currentGroupKey = groupKey
}
groupedItems.add(item)
}
return groupedItems
}
private fun getDateGroup(timestamp: Long): Pair<String, UiContentItem.Group> {
val zoneId = ZoneId.systemDefault()
val itemDate = Instant.ofEpochSecond(timestamp)
.atZone(zoneId)
.toLocalDate()
val today = LocalDate.now(zoneId)
val daysAgo = ChronoUnit.DAYS.between(itemDate, today)
val todayGroup = UiContentItem.Group.Today()
val yesterdayGroup = UiContentItem.Group.Yesterday()
val previous7DaysGroup = UiContentItem.Group.Previous7Days()
val previous14DaysGroup = UiContentItem.Group.Previous14Days()
return when {
daysAgo == 0L -> todayGroup.id to todayGroup
daysAgo == 1L -> yesterdayGroup.id to yesterdayGroup
daysAgo in 2..7 -> previous7DaysGroup.id to previous7DaysGroup
daysAgo in 8..14 -> previous14DaysGroup.id to previous14DaysGroup
itemDate.year == today.year -> {
val monthName =
itemDate.month.getDisplayName(TextStyle.FULL, localeProvider.locale())
monthName to UiContentItem.Group.Month(id = monthName, title = monthName)
}
else -> {
val monthAndYear = "${
itemDate.month.getDisplayName(
TextStyle.FULL,
localeProvider.locale()
)
} ${itemDate.year}"
monthAndYear to UiContentItem.Group.MonthAndYear(
id = monthAndYear,
title = monthAndYear
)
}
}
}
// Function to create search parameters
private fun createSearchParams(
activeTab: AllContentTab,
activeQuery: String,
): SearchObjects.Params {
val filters = activeTab.filtersForSearch(
spaces = listOf(vmParams.spaceId.id)
)
return SearchObjects.Params(
filters = filters,
keys = listOf(Relations.ID),
fulltext = activeQuery
)
}
// 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() {
viewModelScope.launch {
combine(
_modeState,
_sortState
) { mode, sort ->
mode to sort
}.collectLatest { (mode, sort) ->
val uiMode = listOf(
AllContentMenuMode.AllContent(isSelected = mode == AllContentMode.AllContent),
AllContentMenuMode.Unlinked(isSelected = mode == AllContentMode.Unlinked)
)
val container = MenuSortsItem.Container(sort = sort)
val uiSorts = listOf(
MenuSortsItem.Sort(
sort = AllContentSort.ByName(isSelected = sort is AllContentSort.ByName)
),
MenuSortsItem.Sort(
sort = AllContentSort.ByDateUpdated(isSelected = sort is AllContentSort.ByDateUpdated)
),
MenuSortsItem.Sort(
sort = AllContentSort.ByDateCreated(isSelected = sort is AllContentSort.ByDateCreated)
)
)
val uiSortTypes = listOf(
MenuSortsItem.SortType(
sort = sort,
sortType = DVSortType.ASC,
isSelected = sort.sortType == DVSortType.ASC
),
MenuSortsItem.SortType(
sort = sort,
sortType = DVSortType.DESC,
isSelected = sort.sortType == DVSortType.DESC
)
)
_uiMenu.value = UiMenuState(
mode = uiMode,
container = container,
sorts = uiSorts,
types = uiSortTypes
)
}
}
}
fun onTabClicked(tab: AllContentTab) {
Timber.d("onTabClicked: $tab")
if (tab == AllContentTab.TYPES || tab == AllContentTab.RELATIONS) {
viewModelScope.launch {
_commands.emit(Command.SendToast("Not implemented yet"))
}
return
}
_tabsState.value = tab
}
fun onAllContentModeClicked(mode: AllContentMenuMode) {
Timber.d("onAllContentModeClicked: $mode")
_modeState.value = when (mode) {
is AllContentMenuMode.AllContent -> AllContentMode.AllContent
is AllContentMenuMode.Unlinked -> AllContentMode.Unlinked
}
}
fun onSortClicked(sort: AllContentSort) {
Timber.d("onSortClicked: $sort")
_sortState.value = sort
}
fun onFilterChanged(filter: String) {
Timber.d("onFilterChanged: $filter")
userInput.value = filter
}
fun onLimitUpdated(limit: Int) {
Timber.d("onLimitUpdated: $limit")
_limitState.value = limit
}
fun onItemClicked(item: UiContentItem.Item) {
Timber.d("onItemClicked: ${item.id}")
val layout = item.layout ?: return
viewModelScope.launch {
when (val navigation = layout.navigation(
target = item.id,
space = vmParams.spaceId.id
)) {
is OpenObjectNavigation.OpenDataView -> {
_commands.emit(
Command.NavigateToSetOrCollection(
id = navigation.target,
space = navigation.space
)
)
}
is OpenObjectNavigation.OpenEditor -> {
_commands.emit(
Command.NavigateToEditor(
id = navigation.target,
space = navigation.space
)
)
}
is OpenObjectNavigation.UnexpectedLayoutError -> {
_commands.emit(Command.SendToast("Unexpected layout: ${navigation.layout}"))
}
}
}
}
fun onStop() {
Timber.d("onStop")
viewModelScope.launch {
storelessSubscriptionContainer.unsubscribe(listOf(subscriptionId()))
}
}
data class VmParams(
val spaceId: SpaceId,
val useHistory: Boolean = true
)
internal data class Result(
val mode: AllContentMode,
val tab: AllContentTab,
val sort: AllContentSort,
val limitedObjectIds: List<String>,
val limit: Int
)
sealed class Command {
data class NavigateToEditor(val id: Id, val space: Id) : Command()
data class NavigateToSetOrCollection(val id: Id, val space: Id) : Command()
data class SendToast(val message: String) : Command()
}
companion object {
const val DEFAULT_DEBOUNCE_DURATION = 300L
const val DEFAULT_LOADING_DELAY = 250L
//INITIAL STATE
const val DEFAULT_SEARCH_LIMIT = 50
val DEFAULT_INITIAL_TAB = AllContentTab.PAGES
val DEFAULT_INITIAL_SORT = AllContentSort.ByDateCreated()
val DEFAULT_INITIAL_MODE = AllContentMode.AllContent
val DEFAULT_QUERY = ""
}
}

View file

@ -0,0 +1,47 @@
package com.anytypeio.anytype.feature_allcontent.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.all_content.RestoreAllContentState
import com.anytypeio.anytype.domain.all_content.UpdateAllContentState
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,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val updateAllContentState: UpdateAllContentState,
private val restoreAllContentState: RestoreAllContentState,
private val searchObjects: SearchObjects,
private val localeProvider: LocaleProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
AllContentViewModel(
vmParams = vmParams,
storeOfObjectTypes = storeOfObjectTypes,
storeOfRelations = storeOfRelations,
urlBuilder = urlBuilder,
analytics = analytics,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
storelessSubscriptionContainer = storelessSubscriptionContainer,
restoreAllContentState = restoreAllContentState,
updateAllContentState = updateAllContentState,
searchObjects = searchObjects,
localeProvider = localeProvider
) as T
}

View file

@ -0,0 +1,238 @@
package com.anytypeio.anytype.feature_allcontent.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter.Companion.tint
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.UXBody
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.MenuSortsItem
import com.anytypeio.anytype.feature_allcontent.models.UiMenuState
@Composable
fun AllContentMenu(
uiMenuState: UiMenuState,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit
) {
var sortingExpanded by remember { mutableStateOf(false) }
uiMenuState.mode.forEach { item ->
MenuItem(
title = getModeTitle(item),
isSelected = item.isSelected,
modifier = Modifier.clickable {
onModeClick(item)
}
)
}
Spacer(modifier = Modifier.height(8.dp))
SortingBox(
modifier = Modifier
.clickable {
sortingExpanded = !sortingExpanded
},
subtitle = uiMenuState.container.sort.title()
)
if (sortingExpanded) {
uiMenuState.sorts.forEach { item ->
MenuItem(
title = item.sort.title(),
isSelected = item.sort.isSelected,
modifier = Modifier
.clickable {
onSortClick(item.sort)
}
)
}
uiMenuState.types.forEach { item ->
MenuItem(
title = item.sortType.title(item.sort),
isSelected = item.isSelected,
modifier = Modifier
.clickable {
val updatedSort = when (item.sort) {
is AllContentSort.ByName -> item.sort.copy(sortType = item.sortType)
is AllContentSort.ByDateCreated -> item.sort.copy(sortType = item.sortType)
is AllContentSort.ByDateUpdated -> item.sort.copy(sortType = item.sortType)
}
onSortClick(updatedSort)
}
)
}
}
}
@Composable
private fun SortingBox(modifier: Modifier, subtitle: String) {
Row(
modifier = modifier
.fillMaxWidth()
.background(colorResource(id = R.color.background_secondary)),
verticalAlignment = CenterVertically
) {
Image(
modifier = Modifier.size(32.dp),
painter = painterResource(R.drawable.ic_menu_arrow_right),
contentDescription = "",
colorFilter = tint(colorResource(id = R.color.glyph_selected))
)
Column(
modifier = Modifier
.wrapContentHeight()
.padding(top = 11.dp, bottom = 10.dp, start = 6.dp)
) {
Text(
text = stringResource(id = R.string.all_content_sort_by),
modifier = Modifier.wrapContentSize(),
style = UXBody,
color = colorResource(id = R.color.text_primary)
)
Text(
text = subtitle,
modifier = Modifier.wrapContentSize(),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_secondary)
)
}
}
}
@Composable
private fun MenuItem(modifier: Modifier, title: String, isSelected: Boolean) {
Row(
modifier = modifier
.fillMaxWidth()
.height(44.dp)
.background(colorResource(id = R.color.background_secondary)),
verticalAlignment = CenterVertically,
) {
Image(
modifier = Modifier
.wrapContentSize()
.padding(start = 12.dp),
painter = painterResource(R.drawable.ic_check_16),
contentDescription = "All Content mode selected",
alpha = if (isSelected) 1f else 0f
)
Text(
text = title,
modifier = Modifier
.wrapContentSize()
.padding(start = 8.dp),
style = UXBody,
color = colorResource(id = R.color.text_primary)
)
}
}
//region RESOURCES
@Composable
private fun getModeTitle(mode: AllContentMenuMode): String = stringResource(
when (mode) {
is AllContentMenuMode.AllContent -> R.string.all_content_title_all_content
is AllContentMenuMode.Unlinked -> R.string.all_content_title_only_unlinked
}
)
@Composable
private fun AllContentSort.title(): String = stringResource(
when (this) {
is AllContentSort.ByDateCreated -> R.string.all_content_sort_date_created
is AllContentSort.ByDateUpdated -> R.string.all_content_sort_date_updated
is AllContentSort.ByName -> R.string.all_content_sort_name
}
)
@Composable
private fun DVSortType.title(sort: AllContentSort): String = when (this) {
DVSortType.ASC -> {
when (sort) {
is AllContentSort.ByDateCreated, is AllContentSort.ByDateUpdated -> stringResource(
id = R.string.all_content_sort_date_asc
)
is AllContentSort.ByName -> stringResource(id = R.string.all_content_sort_name_asc)
}
}
DVSortType.DESC -> {
when (sort) {
is AllContentSort.ByDateCreated, is AllContentSort.ByDateUpdated -> stringResource(
id = R.string.all_content_sort_date_desc
)
is AllContentSort.ByName -> stringResource(id = R.string.all_content_sort_name_desc)
}
}
DVSortType.CUSTOM -> ""
}
//endregion
//region PREVIEW
@DefaultPreviews
@Composable
fun AllContentMenuPreview() {
AllContentMenu(
uiMenuState = UiMenuState(
mode = listOf(
AllContentMenuMode.AllContent(isSelected = true),
AllContentMenuMode.Unlinked(isSelected = false)
),
sorts = listOf(
MenuSortsItem.Sort(
sort = AllContentSort.ByName(isSelected = true)
),
MenuSortsItem.Sort(
AllContentSort.ByDateUpdated(isSelected = false)
),
MenuSortsItem.Sort(
AllContentSort.ByDateCreated(isSelected = false)
)
),
types = listOf(
MenuSortsItem.SortType(
sortType = DVSortType.ASC,
isSelected = true,
sort = AllContentSort.ByName(isSelected = true)
),
MenuSortsItem.SortType(
sortType = DVSortType.DESC,
isSelected = false,
sort = AllContentSort.ByName(isSelected = false)
),
),
container = MenuSortsItem.Container(AllContentSort.ByName())
),
onModeClick = {},
onSortClick = {}
)
}
//endregion

View file

@ -0,0 +1,417 @@
package com.anytypeio.anytype.feature_allcontent.ui
import android.os.Build
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.UXBody
import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator
import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs
import com.anytypeio.anytype.core_ui.widgets.DefaultBasicAvatarIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultEmojiObjectIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultFileObjectImageIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultObjectBookmarkIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultObjectImageIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultProfileAvatarIcon
import com.anytypeio.anytype.core_ui.widgets.DefaultProfileIconImage
import com.anytypeio.anytype.core_ui.widgets.DefaultTaskObjectIcon
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.feature_allcontent.BuildConfig
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.UiMenuState
import com.anytypeio.anytype.feature_allcontent.models.UiTabsState
import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
@Composable
fun AllContentWrapperScreen(
uiTitleState: UiTitleState,
uiTabsState: UiTabsState,
uiMenuButtonViewState: MenuButtonViewState,
uiMenuState: UiMenuState,
uiState: UiContentState,
onTabClick: (AllContentTab) -> Unit,
onQueryChanged: (String) -> Unit,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
onItemClicked: (UiContentItem.Item) -> Unit
) {
val objects = remember { mutableStateOf<List<UiContentItem>>(emptyList()) }
if (uiState is UiContentState.Content) {
objects.value = uiState.items
}
AllContentMainScreen(
uiTitleState = uiTitleState,
uiTabsState = uiTabsState,
uiMenuButtonViewState = uiMenuButtonViewState,
onTabClick = onTabClick,
objects = objects,
isLoading = uiState is UiContentState.Loading,
onQueryChanged = onQueryChanged,
uiMenuState = uiMenuState,
onModeClick = onModeClick,
onSortClick = onSortClick,
onItemClicked = onItemClicked
)
}
@Composable
fun AllContentMainScreen(
uiTitleState: UiTitleState,
uiTabsState: UiTabsState,
uiMenuButtonViewState: MenuButtonViewState,
uiMenuState: UiMenuState,
objects: MutableState<List<UiContentItem>>,
onTabClick: (AllContentTab) -> Unit,
onQueryChanged: (String) -> Unit,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit,
isLoading: Boolean,
onItemClicked: (UiContentItem.Item) -> Unit
) {
val modifier = Modifier
.background(color = colorResource(id = R.color.background_primary))
Scaffold(
modifier = modifier
.fillMaxSize(),
containerColor = colorResource(id = R.color.background_primary),
topBar = {
Column(
modifier = if (BuildConfig.USE_EDGE_TO_EDGE && Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK)
Modifier
.windowInsetsPadding(WindowInsets.statusBars)
.fillMaxWidth()
else
Modifier.fillMaxWidth()
) {
if (uiTitleState !is UiTitleState.Hidden) {
AllContentTopBarContainer(
titleState = uiTitleState,
menuButtonState = uiMenuButtonViewState,
uiMenuState = uiMenuState,
onSortClick = onSortClick,
onModeClick = onModeClick,
)
}
if (uiTabsState is UiTabsState.Default) {
AllContentTabs(tabsViewState = uiTabsState) { tab ->
onTabClick(tab)
}
}
Spacer(modifier = Modifier.size(10.dp))
AllContentSearchBar(onQueryChanged)
Spacer(modifier = Modifier.size(10.dp))
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
}
},
content = { paddingValues ->
val contentModifier =
if (BuildConfig.USE_EDGE_TO_EDGE && Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK)
Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxSize()
.padding(paddingValues)
else
Modifier
.fillMaxSize()
.padding(paddingValues)
if (isLoading) {
Box(modifier = contentModifier) {
LoadingState()
}
} else {
ContentItems(
modifier = contentModifier,
items = objects.value,
onItemClicked = onItemClicked
)
}
}
)
}
@Composable
private fun ContentItems(
modifier: Modifier,
items: List<UiContentItem>,
onItemClicked: (UiContentItem.Item) -> Unit
) {
LazyColumn(modifier = modifier) {
items(
count = items.size,
key = { index -> items[index].id },
contentType = { index ->
when (items[index]) {
is UiContentItem.Group -> "group"
is UiContentItem.Item -> "item"
}
}
) { index ->
when (val item = items[index]) {
is UiContentItem.Group -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
contentAlignment = Alignment.BottomStart
) {
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 20.dp, bottom = 8.dp),
text = item.title(),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary),
)
}
}
is UiContentItem.Item -> {
Item(
modifier = Modifier
.padding(horizontal = 16.dp)
.bottomBorder()
.animateItem()
.clickable {
onItemClicked(item)
},
item = item
)
}
}
}
}
}
@Composable
private fun BoxScope.LoadingState() {
val loadingAlpha by animateFloatAsState(targetValue = 1f, label = "")
DotsLoadingIndicator(
animating = true,
modifier = Modifier
.graphicsLayer { alpha = loadingAlpha }
.align(Alignment.Center),
animationSpecs = FadeAnimationSpecs(itemCount = 3),
color = colorResource(id = R.color.glyph_active),
size = ButtonSize.Small
)
}
@DefaultPreviews
@Composable
fun PreviewLoadingState() {
Box(modifier = Modifier.fillMaxSize()) {
LoadingState()
}
}
@Composable
private fun Item(
modifier: Modifier,
item: UiContentItem.Item
) {
Row(
modifier = modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.padding(0.dp, 12.dp, 12.dp, 12.dp)
.size(48.dp)
.align(CenterVertically)
) {
AllContentItemIcon(icon = item.icon, modifier = Modifier)
}
Column(
modifier = Modifier
.align(CenterVertically)
.padding(0.dp, 0.dp, 60.dp, 0.dp)
) {
val name = item.name.trim().ifBlank { stringResource(R.string.untitled) }
Text(
text = name,
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
val description = item.description
if (!description.isNullOrBlank()) {
Text(
text = description,
style = Relations3,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
val typeName = item.typeName
if (!typeName.isNullOrBlank()) {
Text(
text = typeName,
style = Relations3,
color = colorResource(id = R.color.text_secondary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Composable
fun AllContentItemIcon(
icon: ObjectIcon,
modifier: Modifier,
iconSize: Dp = 48.dp,
onTaskIconClicked: (Boolean) -> Unit = {},
avatarBackgroundColor: Int = R.color.shape_secondary,
avatarFontSize: TextUnit = 28.sp,
avatarTextStyle: TextStyle = TextStyle(
fontWeight = FontWeight.SemiBold,
color = colorResource(id = R.color.text_white)
)
) {
when (icon) {
is ObjectIcon.Profile.Avatar -> DefaultProfileAvatarIcon(
modifier = modifier,
iconSize = iconSize,
icon = icon,
avatarTextStyle = avatarTextStyle,
avatarFontSize = avatarFontSize,
avatarBackgroundColor = avatarBackgroundColor
)
is ObjectIcon.Profile.Image -> DefaultProfileIconImage(icon, modifier, iconSize)
is ObjectIcon.Basic.Emoji -> DefaultEmojiObjectIcon(modifier, iconSize, icon)
is ObjectIcon.Basic.Image -> DefaultObjectImageIcon(icon.hash, modifier, iconSize)
is ObjectIcon.Basic.Avatar -> DefaultBasicAvatarIcon(modifier, iconSize, icon)
is ObjectIcon.Bookmark -> DefaultObjectBookmarkIcon(icon.image, modifier, iconSize)
is ObjectIcon.Task -> DefaultTaskObjectIcon(modifier, iconSize, icon, onTaskIconClicked)
is ObjectIcon.File -> {
DefaultFileObjectImageIcon(
fileName = icon.fileName.orEmpty(),
mime = icon.mime.orEmpty(),
modifier = modifier,
iconSize = iconSize,
extension = icon.extensions
)
}
else -> {
// Draw nothing.
}
}
}
@Composable
private fun BoxScope.ErrorState(message: String) {
Text(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
text = "Error : message",
color = colorResource(id = R.color.palette_system_red),
style = UXBody
)
}
@Composable
fun UiContentItem.Group.title(): String {
return when (this) {
is UiContentItem.Group.Today -> stringResource(R.string.allContent_group_today)
is UiContentItem.Group.Yesterday -> stringResource(R.string.allContent_group_yesterday)
is UiContentItem.Group.Previous7Days -> stringResource(R.string.allContent_group_prev_7)
is UiContentItem.Group.Previous14Days -> stringResource(R.string.allContent_group_prev_14)
is UiContentItem.Group.Month -> title
is UiContentItem.Group.MonthAndYear -> title
}
}
object AllContentNavigation {
const val ALL_CONTENT_MAIN = "all_content_main"
}
@Composable
fun Modifier.bottomBorder(
strokeWidth: Dp = 0.5.dp,
color: Color = colorResource(R.color.shape_primary)
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx / 2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)

View file

@ -0,0 +1,411 @@
package com.anytypeio.anytype.feature_allcontent.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.bouncingClickable
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.views.Title2
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
import com.anytypeio.anytype.feature_allcontent.models.UiTitleState
//region AllContentTopBarContainer
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AllContentTopBarContainer(
titleState: UiTitleState,
menuButtonState: MenuButtonViewState,
uiMenuState: UiMenuState,
onModeClick: (AllContentMenuMode) -> Unit,
onSortClick: (AllContentSort) -> Unit
) {
var isMenuExpanded by remember { mutableStateOf(false) }
CenterAlignedTopAppBar(
modifier = Modifier.fillMaxWidth(),
expandedHeight = 48.dp,
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 = 20.dp,
) {
AllContentMenu(
uiMenuState = uiMenuState,
onModeClick = {
onModeClick(it)
isMenuExpanded = false
},
onSortClick = {
onSortClick(it)
isMenuExpanded = false
}
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = colorResource(id = R.color.background_primary)
),
)
}
@DefaultPreviews
@Composable
private fun AllContentTopBarContainerPreview() {
AllContentTopBarContainer(
titleState = UiTitleState.OnlyUnlinked,
menuButtonState = MenuButtonViewState.Visible,
uiMenuState = UiMenuState(
mode = listOf(
AllContentMenuMode.AllContent(isSelected = true),
AllContentMenuMode.Unlinked()
),
container = MenuSortsItem.Container(
sort = AllContentSort.ByName(isSelected = true)
),
sorts = listOf(
MenuSortsItem.Sort(
sort = AllContentSort.ByName(isSelected = true)
),
),
types = listOf(
MenuSortsItem.SortType(
sort = AllContentSort.ByName(isSelected = true),
sortType = DVSortType.DESC,
isSelected = true
),
MenuSortsItem.SortType(
sort = AllContentSort.ByDateCreated(isSelected = false),
sortType = DVSortType.ASC,
isSelected = false
),
)
),
onModeClick = {},
onSortClick = {}
)
}
//endregion
//region AllContentTitle
@Composable
fun AllContentTitle(state: UiTitleState) {
when (state) {
UiTitleState.Hidden -> return
UiTitleState.AllContent -> {
Text(
modifier = Modifier
.wrapContentSize(),
text = stringResource(id = R.string.all_content_title_all_content),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
UiTitleState.OnlyUnlinked -> {
Text(
modifier = Modifier
.wrapContentSize(),
text = stringResource(id = R.string.all_content_title_only_unlinked),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
}
}
//endregion
//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
)
}
}
}
//endregion
//region AllContentTabs
@Composable
fun AllContentTabs(
tabsViewState: UiTabsState.Default,
onClick: (AllContentTab) -> Unit
) {
val scrollState = rememberLazyListState()
var selectedTab by remember { mutableStateOf(tabsViewState.selectedTab) }
val snapFlingBehavior = rememberSnapFlingBehavior(scrollState)
LazyRow(
state = scrollState,
flingBehavior = snapFlingBehavior,
modifier = Modifier
.fillMaxWidth()
.height(40.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(start = 20.dp, end = 20.dp)
) {
items(
count = tabsViewState.tabs.size,
key = { index -> tabsViewState.tabs[index].ordinal },
) { index ->
val tab = tabsViewState.tabs[index]
AllContentTabText(
tab = tab,
isSelected = tab == selectedTab,
onClick = {
selectedTab = tab
onClick(tab)
}
)
}
}
}
@Composable
private fun AllContentTabText(
tab: AllContentTab,
isSelected: Boolean,
onClick: () -> Unit
) {
Text(
modifier = Modifier
.wrapContentSize()
.noRippleClickable { onClick() },
text = getTabText(tab),
style = Title2,
color = if (isSelected) colorResource(id = R.color.glyph_button) else colorResource(id = R.color.glyph_active),
maxLines = 1
)
}
@Composable
private fun getTabText(tab: AllContentTab): String {
return when (tab) {
AllContentTab.PAGES -> stringResource(id = R.string.all_content_title_tab_pages)
AllContentTab.FILES -> stringResource(id = R.string.all_content_title_tab_files)
AllContentTab.MEDIA -> stringResource(id = R.string.all_content_title_tab_media)
AllContentTab.BOOKMARKS -> stringResource(id = R.string.all_content_title_tab_bookmarks)
AllContentTab.TYPES -> stringResource(id = R.string.all_content_title_tab_objetc_types)
AllContentTab.RELATIONS -> stringResource(id = R.string.all_content_title_tab_relations)
AllContentTab.LISTS -> stringResource(id = R.string.all_content_title_tab_lists)
}
}
@DefaultPreviews
@Composable
private fun AllContentTabsPreview() {
AllContentTabs(
tabsViewState = UiTabsState.Default(
tabs = listOf(
AllContentTab.PAGES,
AllContentTab.FILES,
AllContentTab.MEDIA,
AllContentTab.BOOKMARKS,
AllContentTab.TYPES,
AllContentTab.RELATIONS
),
selectedTab = AllContentTab.MEDIA
),
onClick = {}
)
}
//endregion
//region SearchBar
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AllContentSearchBar(onQueryChanged: (String) -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
val focus = LocalFocusManager.current
val focusRequester = FocusRequester()
val selectionColors = TextSelectionColors(
backgroundColor = colorResource(id = R.color.cursor_color).copy(
alpha = 0.2f
),
handleColor = colorResource(id = R.color.cursor_color),
)
var query by remember { mutableStateOf(TextFieldValue()) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = colorResource(id = R.color.shape_transparent),
shape = RoundedCornerShape(10.dp)
)
.height(40.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_search_18),
contentDescription = "Search icon",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(
start = 11.dp
)
)
CompositionLocalProvider(value = LocalTextSelectionColors provides selectionColors) {
BasicTextField(
value = query,
modifier = Modifier
.weight(1.0f)
.padding(start = 6.dp)
.align(Alignment.CenterVertically)
.focusRequester(focusRequester),
textStyle = BodyRegular.copy(
color = colorResource(id = R.color.text_primary)
),
onValueChange = { input ->
query = input.also {
onQueryChanged(input.text)
}
},
singleLine = true,
maxLines = 1,
keyboardActions = KeyboardActions(
onDone = {
focus.clearFocus(true)
}
),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = query.text,
innerTextField = innerTextField,
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
placeholder = {
Text(
text = stringResource(id = R.string.search),
style = BodyRegular.copy(
color = colorResource(id = R.color.text_tertiary)
)
)
},
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = colorResource(id = R.color.cursor_color),
),
border = {},
contentPadding = PaddingValues()
)
},
cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)),
)
}
Spacer(Modifier.width(9.dp))
AnimatedVisibility(
visible = query.text.isNotEmpty(),
enter = fadeIn(tween(100)),
exit = fadeOut(tween(100))
) {
Image(
painter = painterResource(id = R.drawable.ic_clear_18),
contentDescription = "Clear icon",
modifier = Modifier
.padding(end = 9.dp)
.noRippleClickable {
query = TextFieldValue().also {
onQueryChanged("")
}
}
)
}
}
}
@DefaultPreviews
@Composable
private fun AllContentSearchBarPreview() {
AllContentSearchBar() {}
}
//endregion