1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +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

@ -162,6 +162,7 @@ dependencies {
implementation project(':payments')
implementation project(':feature-discussions')
implementation project(':gallery-experience')
implementation project(':feature-all-content')
//Compile time dependencies
ksp libs.daggerCompiler
@ -197,6 +198,7 @@ dependencies {
implementation libs.lifecycleCompose
implementation libs.compose
implementation libs.fragmentCompose
implementation libs.composeFoundation
implementation libs.composeMaterial
implementation libs.composeMaterial3

View file

@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.di.feature.CreateBookmarkModule
import com.anytypeio.anytype.di.feature.CreateObjectModule
import com.anytypeio.anytype.di.feature.DaggerAllContentComponent
import com.anytypeio.anytype.di.feature.DaggerAppPreferencesComponent
import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent
import com.anytypeio.anytype.di.feature.DaggerSplashComponent
@ -99,6 +100,7 @@ import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectModule
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetSourceModule
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetTypeModule
import com.anytypeio.anytype.di.main.MainComponent
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
@ -329,6 +331,12 @@ class ComponentManager(
.create(params, findComponentDependencies())
}
val allContentComponent = ComponentWithParams { params: AllContentViewModel.VmParams ->
DaggerAllContentComponent
.factory()
.create(params, findComponentDependencies())
}
val objectSetComponent = ComponentMapWithParam { param: DefaultComponentParam ->
main.objectSetComponentBuilder()
.module(ObjectSetModule)

View file

@ -0,0 +1,121 @@
package com.anytypeio.anytype.di.feature
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.all_content.RestoreAllContentState
import com.anytypeio.anytype.domain.all_content.UpdateAllContentState
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.debugging.Logger
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.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModelFactory
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.ui.allcontent.AllContentFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Named
@Component(
dependencies = [AllContentDependencies::class],
modules = [
AllContentModule::class,
AllContentModule.Declarations::class
]
)
@PerScreen
interface AllContentComponent {
@Component.Factory
interface Factory {
fun create(
@BindsInstance vmParams: AllContentViewModel.VmParams,
dependencies: AllContentDependencies
): AllContentComponent
}
fun inject(fragment: AllContentFragment)
}
@Module
object AllContentModule {
@JvmStatic
@Provides
@PerScreen
fun provideStoreLessSubscriptionContainer(
repo: BlockRepository,
channel: SubscriptionEventChannel,
dispatchers: AppCoroutineDispatchers,
logger: Logger
): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl(
repo = repo,
channel = channel,
dispatchers = dispatchers,
logger = logger
)
@JvmStatic
@Provides
@PerScreen
fun provideUpdateAllContentState(
dispatchers: AppCoroutineDispatchers,
userSettingsRepository: UserSettingsRepository
): UpdateAllContentState = UpdateAllContentState(
dispatchers = dispatchers,
settings = userSettingsRepository
)
@JvmStatic
@Provides
@PerScreen
fun provideRestoreAllContentState(
dispatchers: AppCoroutineDispatchers,
userSettingsRepository: UserSettingsRepository
): RestoreAllContentState = RestoreAllContentState(
dispatchers = dispatchers,
settings = userSettingsRepository
)
@JvmStatic
@Provides
@PerScreen
fun searchObjects(
repo: BlockRepository
): SearchObjects = SearchObjects(repo = repo)
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(
factory: AllContentViewModelFactory
): ViewModelProvider.Factory
}
}
interface AllContentDependencies : ComponentDependencies {
fun blockRepository(): BlockRepository
fun analytics(): Analytics
fun urlBuilder(): UrlBuilder
fun dispatchers(): AppCoroutineDispatchers
fun storeOfObjectTypes(): StoreOfObjectTypes
fun storeOfRelations(): StoreOfRelations
fun analyticsHelper(): AnalyticSpaceHelperDelegate
fun userSettingsRepository(): UserSettingsRepository
fun subEventChannel(): SubscriptionEventChannel
fun logger(): Logger
fun localeProvider(): LocaleProvider
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.main
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.di.common.ComponentDependenciesKey
import com.anytypeio.anytype.di.feature.AllContentDependencies
import com.anytypeio.anytype.di.feature.AppPreferencesDependencies
import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectDependencies
import com.anytypeio.anytype.di.feature.CreateBookmarkSubComponent
@ -126,7 +127,8 @@ interface MainComponent :
NotificationDependencies,
GlobalSearchDependencies,
MembershipUpdateComponentDependencies,
VaultComponentDependencies
VaultComponentDependencies,
AllContentDependencies
{
fun inject(app: AndroidApplication)
@ -352,4 +354,9 @@ abstract class ComponentDependenciesModule {
@IntoMap
@ComponentDependenciesKey(VaultComponentDependencies::class)
abstract fun provideVaultComponentDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(AllContentDependencies::class)
abstract fun provideAllContentDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.widgets.collection.Subscription
import com.anytypeio.anytype.ui.allcontent.AllContentFragment
import com.anytypeio.anytype.ui.auth.account.DeletedAccountFragment
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.editor.EditorModalFragment
@ -239,4 +240,11 @@ class Navigator : AppNavigation {
)
)
}
override fun openAllContent(space: Id) {
navController?.navigate(
resId = R.id.action_open_all_content,
args = AllContentFragment.args(space)
)
}
}

View file

@ -0,0 +1,133 @@
package com.anytypeio.anytype.ui.allcontent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModelFactory
import com.anytypeio.anytype.feature_allcontent.ui.AllContentWrapperScreen
import com.anytypeio.anytype.feature_allcontent.ui.AllContentNavigation.ALL_CONTENT_MAIN
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
class AllContentFragment : BaseComposeFragment() {
@Inject
lateinit var factory: AllContentViewModelFactory
private val vm by viewModels<AllContentViewModel> { factory }
private val space get() = argString(ARG_SPACE)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
MaterialTheme(
typography = typography
) {
AllContentScreenWrapper()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribe(vm.commands) { command ->
when (command) {
is AllContentViewModel.Command.NavigateToEditor -> {
runCatching {
navigation().openDocument(
target = command.id,
space = command.space
)
}
}
is AllContentViewModel.Command.NavigateToSetOrCollection -> {
runCatching {
navigation().openObjectSet(
target = command.id,
space = command.space,
)
}
}
is AllContentViewModel.Command.SendToast -> {
toast(command.message)
}
}
}
}
@Composable
fun AllContentScreenWrapper() {
NavHost(
navController = rememberNavController(),
startDestination = ALL_CONTENT_MAIN
) {
composable(route = ALL_CONTENT_MAIN) {
AllContentWrapperScreen(
uiState = vm.uiState.collectAsStateWithLifecycle().value,
onTabClick = vm::onTabClicked,
onQueryChanged = vm::onFilterChanged,
uiTabsState = vm.uiTabsState.collectAsStateWithLifecycle().value,
uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value,
uiMenuButtonViewState = vm.uiMenuButtonState.collectAsStateWithLifecycle().value,
uiMenuState = vm.uiMenu.collectAsStateWithLifecycle().value,
onSortClick = vm::onSortClicked,
onModeClick = vm::onAllContentModeClicked,
onItemClicked = vm::onItemClicked
)
}
}
}
override fun onStop() {
vm.onStop()
super.onStop()
}
override fun injectDependencies() {
val vmParams = AllContentViewModel.VmParams(spaceId = SpaceId(space))
componentManager().allContentComponent.get(vmParams).inject(this)
}
override fun releaseDependencies() {
componentManager().allContentComponent.release()
}
override fun onApplyWindowRootInsets(view: View) {
if (BuildConfig.USE_EDGE_TO_EDGE && Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
// Do nothing.
} else {
super.onApplyWindowRootInsets(view)
}
}
companion object {
const val KEYBOARD_HIDE_DELAY = 300L
const val ARG_SPACE = "arg.all.content.space"
fun args(space: Id): Bundle = bundleOf(ARG_SPACE to space)
}
}

View file

@ -353,14 +353,17 @@ class HomeScreenFragment : BaseComposeFragment() {
)
}
is Navigation.OpenLibrary -> runCatching {
runCatching {
findNavController().navigate(
R.id.libraryFragment,
args = LibraryFragment.args(destination.space)
)
}.onFailure { e ->
Timber.e(e, "Error while opening space library from widgets")
}
findNavController().navigate(
R.id.libraryFragment,
args = LibraryFragment.args(destination.space)
)
}.onFailure { e ->
Timber.e(e, "Error while opening space library from widgets")
}
is Navigation.OpenAllContent -> runCatching {
navigation().openAllContent(space = destination.space)
}.onFailure { e ->
Timber.e(e, "Error while opening all content from widgets")
}
}
}

View file

@ -0,0 +1,10 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%"
android:toXDelta="0%"
android:duration="200" />
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="200" />
</set>

View file

@ -0,0 +1,10 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%"
android:toXDelta="-70%"
android:duration="200" />
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="200" />
</set>

View file

@ -160,6 +160,12 @@
app:popUpTo="@id/vaultScreen"
app:popUpToInclusive="true"
/>
<action
android:id="@+id/action_open_all_content"
app:destination="@id/allContentScreen"
app:enterAnim="@anim/enter_from_right"
app:exitAnim="@anim/exit_to_left"
/>
</fragment>
<fragment
@ -386,6 +392,12 @@
android:id="@+id/globalSearchScreen"
android:name="com.anytypeio.anytype.ui.search.GlobalSearchFragment"
android:label="GlobalSearchScreen" />
<fragment
android:id="@+id/allContentScreen"
android:name="com.anytypeio.anytype.ui.allcontent.AllContentFragment"
android:label="AllContentScreen" />
<fragment
android:id="@+id/libraryFragment"
android:name="com.anytypeio.anytype.ui.library.LibraryFragment"

View file

@ -54,6 +54,7 @@ object Relations {
const val CREATOR = "creator"
const val SYNC_DATE = "syncDate"
const val SYNC_STATUS = "syncStatus"
const val IS_HIDDEN_DISCOVERY = "isHiddenDiscovery"
const val PAGE_COVER = "pageCover"

View file

@ -0,0 +1,19 @@
package com.anytypeio.anytype.core_ui.common
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.Preview
@Preview(
backgroundColor = 0xFFFFFFFF,
showBackground = true,
uiMode = UI_MODE_NIGHT_NO,
name = "Light Mode"
)
@Preview(
backgroundColor = 0x000000,
showBackground = true,
uiMode = UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
annotation class DefaultPreviews

View file

@ -1,7 +1,18 @@
package com.anytypeio.anytype.core_ui.extensions
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.colorResource
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.R
@ -41,3 +52,41 @@ fun light(
ThemeColor.GREY -> colorResource(id = R.color.palette_light_grey)
ThemeColor.DEFAULT -> colorResource(id = R.color.palette_light_default)
}
@OptIn(ExperimentalFoundationApi::class)
fun Modifier.bouncingClickable(
enabled: Boolean = true,
pressScaleFactor: Float = 0.97f,
pressAlphaFactor: Float = 0.7f,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit,
) = composed {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val animationTransition = updateTransition(isPressed, label = "BouncingClickableTransition")
val scaleFactor by animationTransition.animateFloat(
targetValueByState = { pressed -> if (pressed) pressScaleFactor else 1f },
label = "BouncingClickableScaleFactorTransition",
)
val opacity by animationTransition.animateFloat(
targetValueByState = { pressed -> if (pressed) pressAlphaFactor else 1f },
label = "BouncingClickableAlphaTransition",
)
this
.graphicsLayer {
scaleX = scaleFactor
scaleY = scaleFactor
alpha = opacity
}
.combinedClickable(
interactionSource = interactionSource,
indication = null,
enabled = enabled,
onClick = onClick,
onLongClick = onLongClick,
onDoubleClick = onDoubleClick,
)
}

View file

@ -0,0 +1,29 @@
package com.anytypeio.anytype.domain.all_content
import com.anytypeio.anytype.core_models.DVSort
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
class RestoreAllContentState @Inject constructor(
private val settings: UserSettingsRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<RestoreAllContentState.Params, RestoreAllContentState.Response>(
dispatchers.io
) {
override suspend fun doWork(params: Params): Response {
//todo: implement
return Response(activeSort = null)
}
data class Params(
val spaceId: SpaceId
)
data class Response(
val activeSort: String?
)
}

View file

@ -0,0 +1,24 @@
package com.anytypeio.anytype.domain.all_content
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
class UpdateAllContentState @Inject constructor(
private val settings: UserSettingsRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<UpdateAllContentState.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
//todo: implement
}
data class Params(
val spaceId: SpaceId,
val query: String,
val relatedObjectId: Id?
)
}

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

View file

@ -64,6 +64,7 @@ coilComposeVersion = '2.6.0'
sentryVersion = '7.6.0'
composeQrCodeVersion = '1.0.1'
fragmentComposeVersion = "1.8.3"
[libraries]
middleware = { module = "io.anyproto:anytype-heart-android", version.ref = "middlewareVersion" }
@ -151,6 +152,7 @@ navigationCompose = { module = "androidx.navigation:navigation-compose", version
appUpdater = { module = "com.github.PLPsiSoft:AndroidAppUpdater", version = "9913ce80da7871c84af24b9adc2bf2414ca294f0" }
composeQrCode = { module = "com.lightspark:compose-qr-code", version.ref = "composeQrCodeVersion" }
playBilling = { module = "com.android.billingclient:billing", version = "7.0.0" }
fragmentCompose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragmentComposeVersion" }
[bundles]

View file

@ -1748,6 +1748,35 @@ Please provide specific details of your needs here.</string>
<string name="new_object">New object</string>
<string name="vault_my_spaces">My spaces</string>
<string name="all_content">All content</string>
<string name="all_content_title_all_content">All objects</string>
<string name="all_content_title_only_unlinked">Only unlinked</string>
<string name="all_content_title_only_unlinked_description">Unlinked objects that do not have a direct link or backlink with other objects in the graph.</string>
<string name="all_content_view_bin">View Bin</string>
<string name="all_content_title_tab_pages">Pages</string>
<string name="all_content_title_tab_lists">Lists</string>
<string name="all_content_title_tab_media">Media</string>
<string name="all_content_title_tab_bookmarks">Bookmarks</string>
<string name="all_content_title_tab_files">Files</string>
<string name="all_content_title_tab_objetc_types">Object Types</string>
<string name="all_content_title_tab_relations">Relations</string>
<string name="all_content_sort_by">Sort by</string>
<string name="all_content_sort_name_desc">Z → A</string>
<string name="all_content_sort_name_asc">A → Z</string>
<string name="all_content_sort_date_desc">Newest first</string>
<string name="all_content_sort_date_asc">Oldest first</string>
<string name="all_content_sort_date_updated">Date updated</string>
<string name="all_content_sort_date_created">Date created</string>
<string name="all_content_sort_name">Name</string>
<string name="all_content">All objects</string>
<string name="allContent_group_today">Today</string>
<string name="allContent_group_yesterday">Yesterday</string>
<string name="allContent_group_prev_7">Previous 7 days</string>
<string name="allContent_group_prev_14">Previous 14 days</string>
</resources>

View file

@ -1035,7 +1035,11 @@ class HomeScreenViewModel(
)
}
WidgetView.AllContent.ALL_CONTENT_WIDGET_ID -> {
// TODO Proceed with navigation
navigation(
Navigation.OpenAllContent(
space = space
)
)
}
}
}
@ -2036,6 +2040,7 @@ class HomeScreenViewModel(
data class OpenSet(val ctx: Id, val space: Id, val view: Id?) : Navigation()
data class ExpandWidget(val subscription: Subscription, val space: Id) : Navigation()
data class OpenLibrary(val space: Id) : Navigation()
data class OpenAllContent(val space: Id) : Navigation()
}
class Factory @Inject constructor(

View file

@ -51,6 +51,8 @@ interface AppNavigation {
fun openTemplatesModal(typeId: Id)
fun openAllContent(space: Id)
sealed class Command {
data object Exit : Command()

View file

@ -65,3 +65,4 @@ include ':localization'
include ':payments'
include ':gallery-experience'
include ':feature-discussions'
include ':feature-all-content'