From 823032a7ea71d8cce2deac7f5fe9a9ff00c83a7f Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:07:18 +0200 Subject: [PATCH] DROID-2885 All content | Navigation widget (#1628) --- .../anytype/di/feature/AllContentDI.kt | 38 +++++++ .../ui/allcontent/AllContentFragment.kt | 55 ++++++++++- .../ui/widgets/collection/CollectionScreen.kt | 2 +- app/src/main/res/navigation/graph.xml | 13 ++- .../components/BottomNavigationMenu.kt | 6 +- .../presentation/AllContentViewModel.kt | 79 ++++++++++++++- .../AllContentViewModelFactory.kt | 7 +- .../feature_allcontent/ui/AllContentScreen.kt | 98 ++++++++++++++++--- 8 files changed, 269 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/AllContentDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/AllContentDI.kt index 09a3bfc718..a9d46bdc31 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/AllContentDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/AllContentDI.kt @@ -8,14 +8,18 @@ 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.ConfigStorage import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.launch.GetDefaultObjectType 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.page.CreateObject import com.anytypeio.anytype.domain.search.SearchObjects import com.anytypeio.anytype.domain.search.SubscriptionEventChannel +import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModelFactory import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate @@ -93,6 +97,38 @@ object AllContentModule { repo: BlockRepository ): SearchObjects = SearchObjects(repo = repo) + @JvmStatic + @Provides + @PerScreen + fun createObject( + repo: BlockRepository, + getDefaultObjectType: GetDefaultObjectType, + dispatchers: AppCoroutineDispatchers, + spaceManager: SpaceManager, + ): CreateObject = CreateObject( + repo = repo, + getDefaultObjectType = getDefaultObjectType, + dispatchers = dispatchers, + spaceManager = spaceManager + ) + + @JvmStatic + @Provides + @PerScreen + fun provideGetDefaultPageType( + userSettingsRepository: UserSettingsRepository, + blockRepository: BlockRepository, + dispatchers: AppCoroutineDispatchers, + spaceManager: SpaceManager, + configStorage: ConfigStorage + ): GetDefaultObjectType = GetDefaultObjectType( + userSettingsRepository = userSettingsRepository, + blockRepository = blockRepository, + dispatchers = dispatchers, + spaceManager = spaceManager, + configStorage = configStorage + ) + @Module interface Declarations { @PerScreen @@ -115,4 +151,6 @@ interface AllContentDependencies : ComponentDependencies { fun subEventChannel(): SubscriptionEventChannel fun logger(): Logger fun localeProvider(): LocaleProvider + fun spaceManager(): SpaceManager + fun config(): ConfigStorage } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt index 43e8591825..2300b89bdd 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt @@ -14,7 +14,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.fragment.findNavController import com.anytypeio.anytype.BuildConfig +import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.argString @@ -30,6 +32,8 @@ import com.anytypeio.anytype.feature_allcontent.ui.AllContentWrapperScreen import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.widgets.collection.Subscription import com.anytypeio.anytype.ui.base.navigation +import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment +import com.anytypeio.anytype.ui.search.GlobalSearchFragment import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject import timber.log.Timber @@ -59,6 +63,32 @@ class AllContentFragment : BaseComposeFragment() { super.onViewCreated(view, savedInstanceState) subscribe(vm.commands) { command -> when (command) { + is AllContentViewModel.Command.ExitToVault -> { + runCatching { + findNavController().navigate(R.id.actionOpenVault) + }.onFailure { e -> + Timber.e(e, "Error while exiting to vault from all content") + } + } + is AllContentViewModel.Command.Back -> { + runCatching { + findNavController().popBackStack() + }.onFailure { e -> + Timber.e(e, "Error while exiting back from all content") + } + } + is AllContentViewModel.Command.OpenGlobalSearch -> { + runCatching { + findNavController().navigate( + resId = R.id.globalSearchScreen, + args = GlobalSearchFragment.args( + space = space + ) + ) + }.onFailure { e -> + Timber.e(e, "Error while opening global search screen from all content") + } + } is AllContentViewModel.Command.NavigateToEditor -> { runCatching { navigation().openDocument( @@ -67,7 +97,7 @@ class AllContentFragment : BaseComposeFragment() { ) }.onFailure { toast("Failed to open document") - Timber.e(it, "Failed to open document") + Timber.e(it, "Failed to open document from all content") } } is AllContentViewModel.Command.NavigateToSetOrCollection -> { @@ -78,7 +108,7 @@ class AllContentFragment : BaseComposeFragment() { ) }.onFailure { toast("Failed to open object set") - Timber.e(it, "Failed to open object set") + Timber.e(it, "Failed to open object set from all content") } } is AllContentViewModel.Command.SendToast -> { @@ -92,7 +122,7 @@ class AllContentFragment : BaseComposeFragment() { ) }.onFailure { toast("Failed to open bin") - Timber.e(it, "Failed to open bin") + Timber.e(it, "Failed to open bin from all content") } } is AllContentViewModel.Command.OpenTypeEditing -> { @@ -105,7 +135,7 @@ class AllContentFragment : BaseComposeFragment() { ) }.onFailure { toast("Failed to open type editing screen") - Timber.e(it, "Failed to open type editing screen") + Timber.e(it, "Failed to open type editing screen from all content") } } } @@ -133,7 +163,22 @@ class AllContentFragment : BaseComposeFragment() { canPaginate = vm.canPaginate.collectAsStateWithLifecycle().value, onUpdateLimitSearch = vm::updateLimit, uiContentState = vm.uiContentState.collectAsStateWithLifecycle().value, - onTypeClicked = vm::onTypeClicked + onTypeClicked = vm::onTypeClicked, + onHomeClicked = vm::onHomeClicked, + onGlobalSearchClicked = vm::onGlobalSearchClicked, + onAddDocClicked = vm::onAddDockClicked, + onCreateObjectLongClicked = { + val dialog = SelectObjectTypeFragment.new( + flow = SelectObjectTypeFragment.FLOW_CREATE_OBJECT, + space = space + ).apply { + onTypeSelected = { + vm.onCreateObjectOfTypeClicked(it) + } + } + dialog.show(childFragmentManager,null) + }, + onBackClicked = vm::onBackClicked ) } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionScreen.kt index 5557cb126b..2c9507df49 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionScreen.kt @@ -141,7 +141,7 @@ fun ScreenContent( SearchBar(vm, uiState) ListView(vm, uiState, stringResource(id = R.string.search_no_results_try)) } - Box(Modifier.align(BottomCenter)) { + Box(Modifier.align(BottomCenter).padding(bottom = 20.dp)) { BottomNavigationMenu( backClick = { vm.onPrevClicked() }, homeClick = { vm.onHomeClicked() }, diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index 217e28113b..5cd61d376f 100644 --- a/app/src/main/res/navigation/graph.xml +++ b/app/src/main/res/navigation/graph.xml @@ -162,10 +162,7 @@ /> + app:destination="@id/allContentScreen" /> + + >(emptyList()) private val sortState = MutableStateFlow(DEFAULT_INITIAL_SORT) @@ -473,11 +478,17 @@ class AllContentViewModel( fun onItemClicked(item: UiContentItem.Item) { Timber.d("onItemClicked: ${item.id}") val layout = item.layout ?: return - viewModelScope.launch { - when (val navigation = layout.navigation( + proceedWithNavigation( + navigation = layout.navigation( target = item.id, space = vmParams.spaceId.id - )) { + ) + ) + } + + private fun proceedWithNavigation(navigation: OpenObjectNavigation) { + viewModelScope.launch { + when (navigation) { is OpenObjectNavigation.OpenDataView -> { commands.emit( Command.NavigateToSetOrCollection( @@ -503,6 +514,61 @@ class AllContentViewModel( } } + fun onHomeClicked() { + Timber.d("onHomeClicked") + viewModelScope.launch { + commands.emit(Command.ExitToVault) + } + } + + fun onGlobalSearchClicked() { + Timber.d("onGlobalSearchClicked") + viewModelScope.launch { + commands.emit(Command.OpenGlobalSearch) + } + } + + fun onAddDockClicked() { + Timber.d("onAddDockClicked") + proceedWithCreateDoc() + } + + fun onBackClicked() { + Timber.d("onBackClicked") + viewModelScope.launch { + commands.emit(Command.Back) + } + } + + fun onCreateObjectOfTypeClicked(objType: ObjectWrapper.Type) { + proceedWithCreateDoc(objType) + } + + private fun proceedWithCreateDoc( + objType: ObjectWrapper.Type? = null + ) { + val startTime = System.currentTimeMillis() + val params = objType?.uniqueKey.getCreateObjectParams(objType?.defaultTemplateId) + viewModelScope.launch { + createObject.async(params).fold( + onSuccess = { result -> + proceedWithNavigation( + navigation = result.obj.navigation() + ) + sendAnalyticsObjectCreateEvent( + analytics = analytics, + route = EventsDictionary.Routes.objCreateLibrary, + startTime = startTime, + objType = objType ?: storeOfObjectTypes.getByKey(result.typeKey.key), + view = EventsDictionary.View.viewHome, + spaceParams = provideParams(space = vmParams.spaceId.id) + ) + }, + onFailure = { e -> Timber.e(e, "Error while creating a new object") } + ) + } + } + fun onTypeClicked(item: UiContentItem.Type) { Timber.d("onTypeClicked: ${item.id}") viewModelScope.launch { @@ -557,6 +623,9 @@ class AllContentViewModel( data class NavigateToBin(val space: Id) : Command() data class SendToast(val message: String) : Command() data class OpenTypeEditing(val item: UiContentItem.Type) : Command() + data object OpenGlobalSearch : Command() + data object ExitToVault : Command() + data object Back : Command() } companion object { diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModelFactory.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModelFactory.kt index cefa74902b..f308ae6302 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModelFactory.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModelFactory.kt @@ -9,6 +9,7 @@ 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.page.CreateObject import com.anytypeio.anytype.domain.search.SearchObjects import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel.VmParams import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate @@ -24,7 +25,8 @@ class AllContentViewModelFactory @Inject constructor( private val updateAllContentState: UpdateAllContentState, private val restoreAllContentState: RestoreAllContentState, private val searchObjects: SearchObjects, - private val localeProvider: LocaleProvider + private val localeProvider: LocaleProvider, + private val createObject: CreateObject ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = @@ -38,6 +40,7 @@ class AllContentViewModelFactory @Inject constructor( restoreAllContentState = restoreAllContentState, updateAllContentState = updateAllContentState, searchObjects = searchObjects, - localeProvider = localeProvider + localeProvider = localeProvider, + createObject = createObject ) as T } \ No newline at end of file diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt index 4f14c1fc19..ccec6bc4bd 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt @@ -12,11 +12,14 @@ 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.ime 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.systemBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -52,6 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.Caption1Regular @@ -99,7 +103,12 @@ fun AllContentWrapperScreen( onBinClick: () -> Unit, canPaginate: Boolean, onUpdateLimitSearch: () -> Unit, - uiContentState: UiContentState + uiContentState: UiContentState, + onHomeClicked: () -> Unit, + onGlobalSearchClicked: () -> Unit, + onAddDocClicked: () -> Unit, + onCreateObjectLongClicked: () -> Unit, + onBackClicked: () -> Unit ) { val lazyListState = rememberLazyListState() @@ -134,7 +143,12 @@ fun AllContentWrapperScreen( uiItemsState = uiItemsState, lazyListState = lazyListState, uiContentState = uiContentState, - onTypeClicked = onTypeClicked + onTypeClicked = onTypeClicked, + onHomeClicked = onHomeClicked, + onGlobalSearchClicked = onGlobalSearchClicked, + onAddDocClicked = onAddDocClicked, + onCreateObjectLongClicked = onCreateObjectLongClicked, + onBackClicked = onBackClicked ) } @@ -153,17 +167,41 @@ fun AllContentMainScreen( onTypeClicked: (UiContentItem.Type) -> Unit, onBinClick: () -> Unit, lazyListState: LazyListState, - uiContentState: UiContentState + uiContentState: UiContentState, + onHomeClicked: () -> Unit, + onGlobalSearchClicked: () -> Unit, + onAddDocClicked: () -> Unit, + onCreateObjectLongClicked: () -> Unit, + onBackClicked: () -> Unit ) { - val modifier = Modifier - .background(color = colorResource(id = R.color.background_primary)) - var isSearchEmpty by remember { mutableStateOf(true) } Scaffold( - modifier = modifier + modifier = Modifier .fillMaxSize(), containerColor = colorResource(id = R.color.background_primary), + bottomBar = { + Box( + modifier = if (BuildConfig.USE_EDGE_TO_EDGE && Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) + Modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(bottom = 20.dp) + .fillMaxWidth() + else + Modifier + .padding(bottom = 20.dp) + .fillMaxWidth() + ) { + BottomMenu( + modifier = Modifier.align(Alignment.BottomCenter), + onHomeClicked = onHomeClicked, + onGlobalSearchClicked = onGlobalSearchClicked, + onAddDocClicked = onAddDocClicked, + onCreateObjectLongClicked = onCreateObjectLongClicked, + onBackClicked = onBackClicked + ) + } + }, topBar = { Column( modifier = if (BuildConfig.USE_EDGE_TO_EDGE && Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) @@ -198,11 +236,13 @@ fun AllContentMainScreen( Modifier .windowInsetsPadding(WindowInsets.navigationBars) .fillMaxSize() - .padding(paddingValues) + .padding(top = paddingValues.calculateTopPadding()) + .background(color = colorResource(id = R.color.background_primary)) else Modifier .fillMaxSize() .padding(paddingValues) + .background(color = colorResource(id = R.color.background_primary)) Box( modifier = contentModifier, @@ -214,18 +254,22 @@ fun AllContentMainScreen( is UiContentState.Error -> { ErrorState(uiContentState.message) } + is UiContentState.Idle -> { // Do nothing. } + UiContentState.InitLoading -> { LoadingState() } + UiContentState.Paging -> {} UiContentState.Empty -> { EmptyState(isSearchEmpty = isSearchEmpty) } } } + else -> { ContentItems( uiItemsState = uiItemsState, @@ -241,6 +285,27 @@ fun AllContentMainScreen( ) } +@Composable +fun BottomMenu( + modifier: Modifier = Modifier, + onHomeClicked: () -> Unit, + onGlobalSearchClicked: () -> Unit, + onAddDocClicked: () -> Unit, + onCreateObjectLongClicked: () -> Unit, + onBackClicked: () -> Unit +) { + val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 + if (isImeVisible) return + BottomNavigationMenu( + modifier = modifier, + backClick = onBackClicked, + onProfileClicked = onHomeClicked, + searchClick = onGlobalSearchClicked, + addDocClick = onAddDocClicked, + onCreateObjectLongClicked = onCreateObjectLongClicked + ) +} + @Composable private fun ContentItems( uiItemsState: List, @@ -329,7 +394,7 @@ private fun ContentItems( LaunchedEffect(key1 = uiContentState) { if (uiContentState is UiContentState.Idle) { - if (uiContentState.scrollToTop) { + if (uiContentState.scrollToTop) { scope.launch { lazyListState.scrollToItem(0) } @@ -366,7 +431,13 @@ fun PreviewMainScreen() { AllContentMainScreen( uiItemsState = emptyList(), uiTitleState = UiTitleState.AllContent, - uiTabsState = UiTabsState(tabs = listOf(AllContentTab.PAGES, AllContentTab.TYPES, AllContentTab.LISTS), selectedTab = AllContentTab.LISTS), + uiTabsState = UiTabsState( + tabs = listOf( + AllContentTab.PAGES, + AllContentTab.TYPES, + AllContentTab.LISTS + ), selectedTab = AllContentTab.LISTS + ), uiMenuState = UiMenuState.Hidden, onTabClick = {}, onQueryChanged = {}, @@ -376,7 +447,12 @@ fun PreviewMainScreen() { onBinClick = {}, lazyListState = rememberLazyListState(), uiContentState = UiContentState.Error("Error message"), - onTypeClicked = {} + onTypeClicked = {}, + onHomeClicked = {}, + onGlobalSearchClicked = {}, + onAddDocClicked = {}, + onCreateObjectLongClicked = {}, + onBackClicked = {} ) }