diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt index fc1b0ffcbb..9004df06d6 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt @@ -59,6 +59,7 @@ import com.anytypeio.anytype.presentation.sets.ObjectSetSession import com.anytypeio.anytype.presentation.sets.ObjectSetViewModelFactory import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubscription +import com.anytypeio.anytype.presentation.sets.viewer.ViewerDelegate import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.test_utils.MockDataFactory @@ -139,6 +140,9 @@ abstract class TestObjectSetSetup { @Mock lateinit var templatesContainer: ObjectTypeTemplatesContainer + @Mock + lateinit var viewerDelegate: ViewerDelegate + private lateinit var getTemplates: GetTemplates private lateinit var getDefaultPageType: GetDefaultPageType @@ -259,7 +263,8 @@ abstract class TestObjectSetSetup { updateDataViewViewer = updateDataViewViewer, templatesContainer = templatesContainer, setObjectListIsArchived = setObjectListIsArchived, - duplicateObjects = duplicateObjects + duplicateObjects = duplicateObjects, + viewerDelegate = viewerDelegate ) } diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index 14b1df80b4..eb634126b5 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -9,13 +9,11 @@ import com.anytypeio.anytype.di.feature.AddObjectRelationValueModule import com.anytypeio.anytype.di.feature.AuthModule import com.anytypeio.anytype.di.feature.CreateAccountModule import com.anytypeio.anytype.di.feature.CreateBookmarkModule -import com.anytypeio.anytype.di.feature.CreateDataViewViewerModule import com.anytypeio.anytype.di.feature.CreateObjectModule import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent import com.anytypeio.anytype.di.feature.DaggerSplashComponent import com.anytypeio.anytype.di.feature.DataViewRelationValueModule import com.anytypeio.anytype.di.feature.DebugSettingsModule -import com.anytypeio.anytype.di.feature.EditDataViewViewerModule import com.anytypeio.anytype.di.feature.EditorSessionModule import com.anytypeio.anytype.di.feature.EditorUseCaseModule import com.anytypeio.anytype.di.feature.KeychainLoginModule @@ -23,7 +21,6 @@ import com.anytypeio.anytype.di.feature.KeychainPhraseModule import com.anytypeio.anytype.di.feature.LinkToObjectModule import com.anytypeio.anytype.di.feature.LinkToObjectOrWebModule import com.anytypeio.anytype.di.feature.MainEntryModule -import com.anytypeio.anytype.di.feature.ManageViewerModule import com.anytypeio.anytype.di.feature.ModifyViewerSortModule import com.anytypeio.anytype.di.feature.MoveToModule import com.anytypeio.anytype.di.feature.ObjectAppearanceIconModule @@ -451,22 +448,6 @@ class ComponentManager( .build() } - val createDataViewViewerComponent = DependentComponentMap { ctx -> - objectSetComponent - .get(ctx) - .createDataViewViewerSubComponent() - .module(CreateDataViewViewerModule) - .build() - } - - val editDataViewViewerComponent = DependentComponentMap { ctx -> - objectSetComponent - .get(ctx) - .editDataViewViewerComponent() - .module(EditDataViewViewerModule) - .build() - } - val dataViewRelationValueComponent = DependentComponentMap { ctx -> objectSetComponent .get(ctx) @@ -539,14 +520,6 @@ class ComponentManager( .build() } - val manageViewerComponent = DependentComponentMap { ctx -> - objectSetComponent - .get(ctx) - .manageViewerComponent() - .module(ManageViewerModule) - .build() - } - val objectsSetSettingsComponent = DependentComponentMap { ctx -> objectSetComponent .get(ctx) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt deleted file mode 100644 index b1e61d71ec..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.anytypeio.anytype.di.feature; - -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.core_utils.di.scope.PerModal -import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewer -import com.anytypeio.anytype.presentation.sets.CreateDataViewViewerViewModel -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import com.anytypeio.anytype.ui.sets.modals.CreateDataViewViewerFragment -import dagger.Module -import dagger.Provides -import dagger.Subcomponent -import kotlinx.coroutines.flow.MutableStateFlow - -@Subcomponent(modules = [CreateDataViewViewerModule::class]) -@PerModal -interface CreateDataViewViewerSubComponent { - @Subcomponent.Builder - interface Builder { - fun module(module: CreateDataViewViewerModule): Builder - fun build(): CreateDataViewViewerSubComponent - } - - fun inject(fragment: CreateDataViewViewerFragment) -} - -@Module -object CreateDataViewViewerModule { - - @JvmStatic - @Provides - @PerModal - fun provideCreateDataViewViewerViewModelFactory( - dispatcher: Dispatcher, - addDataViewViewer: AddDataViewViewer, - analytics: Analytics, - objectState: MutableStateFlow - ): CreateDataViewViewerViewModel.Factory = CreateDataViewViewerViewModel.Factory( - dispatcher = dispatcher, - addDataViewViewer = addDataViewViewer, - analytics = analytics, - objectState = objectState - ) - - @JvmStatic - @Provides - @PerModal - fun provideAddDataViewViewerUseCase( - repo: BlockRepository - ): AddDataViewViewer = AddDataViewViewer(repo = repo) -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt deleted file mode 100644 index 42de96b056..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.anytypeio.anytype.di.feature; - -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.core_utils.di.scope.PerModal -import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.dataview.interactor.* -import com.anytypeio.anytype.presentation.sets.EditDataViewViewerViewModel -import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator -import com.anytypeio.anytype.presentation.sets.ObjectSetSession -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import com.anytypeio.anytype.ui.sets.modals.EditDataViewViewerFragment -import dagger.Module -import dagger.Provides -import dagger.Subcomponent -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -@Subcomponent(modules = [EditDataViewViewerModule::class]) -@PerModal -interface EditDataViewViewerSubComponent { - @Subcomponent.Builder - interface Builder { - fun module(module: EditDataViewViewerModule): Builder - fun build(): EditDataViewViewerSubComponent - } - - fun inject(fragment: EditDataViewViewerFragment) -} - -@Module -object EditDataViewViewerModule { - - @JvmStatic - @Provides - @PerModal - fun provideEditDataViewViewerViewModelFactory( - renameDataViewViewer: RenameDataViewViewer, - deleteDataViewViewer: DeleteDataViewViewer, - duplicateDataViewViewer: DuplicateDataViewViewer, - updateDataViewViewer: UpdateDataViewViewer, - dispatcher: Dispatcher, - state: MutableStateFlow, - objectSetSession: ObjectSetSession, - paginator: ObjectSetPaginator, - analytics: Analytics - ): EditDataViewViewerViewModel.Factory = EditDataViewViewerViewModel.Factory( - renameDataViewViewer = renameDataViewViewer, - deleteDataViewViewer = deleteDataViewViewer, - duplicateDataViewViewer = duplicateDataViewViewer, - updateDataViewViewer = updateDataViewViewer, - dispatcher = dispatcher, - objectState = state, - objectSetSession = objectSetSession, - paginator = paginator, - analytics = analytics - ) - - @JvmStatic - @Provides - @PerModal - fun provideRenameDataViewViewerUseCase( - repo: BlockRepository - ): RenameDataViewViewer = RenameDataViewViewer(repo = repo) - - @JvmStatic - @Provides - @PerModal - fun provideDuplicateDataViewViewerUseCase( - repo: BlockRepository - ): DuplicateDataViewViewer = DuplicateDataViewViewer(repo = repo) - - @JvmStatic - @Provides - @PerModal - fun provideDeleteDataViewViewerUseCase( - repo: BlockRepository - ): DeleteDataViewViewer = DeleteDataViewViewer(repo = repo) -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt deleted file mode 100644 index 0b32d25a83..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.anytypeio.anytype.di.feature; - -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.core_utils.di.scope.PerModal -import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers -import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer -import com.anytypeio.anytype.domain.dataview.interactor.SetDataViewViewerPosition -import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel -import com.anytypeio.anytype.presentation.sets.ObjectSetSession -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import com.anytypeio.anytype.ui.sets.modals.ManageViewerFragment -import dagger.Module -import dagger.Provides -import dagger.Subcomponent -import kotlinx.coroutines.flow.MutableStateFlow - -@Subcomponent(modules = [ManageViewerModule::class]) -@PerModal -interface ManageViewerSubComponent { - @Subcomponent.Builder - interface Builder { - fun module(module: ManageViewerModule): Builder - fun build(): ManageViewerSubComponent - } - - fun inject(fragment: ManageViewerFragment) -} - -@Module -object ManageViewerModule { - - @JvmStatic - @Provides - @PerModal - fun provideManageViewerViewModelFactory( - state: MutableStateFlow, - session: ObjectSetSession, - dispatcher: Dispatcher, - analytics: Analytics, - deleteDataViewViewer: DeleteDataViewViewer, - setDataViewViewerPosition: SetDataViewViewerPosition - ): ManageViewerViewModel.Factory = ManageViewerViewModel.Factory( - objectState = state, - session = session, - dispatcher = dispatcher, - analytics = analytics, - deleteDataViewViewer = deleteDataViewViewer, - setDataViewViewerPosition = setDataViewViewerPosition - ) - - @JvmStatic - @Provides - @PerModal - fun provideDeleteDataViewViewerUseCase( - repo: BlockRepository - ): DeleteDataViewViewer = DeleteDataViewViewer(repo = repo) - - @JvmStatic - @Provides - @PerModal - fun provideSetDataViewViewerPosition( - repo: BlockRepository, - dispatchers: AppCoroutineDispatchers - ): SetDataViewViewerPosition = SetDataViewViewerPosition( - repo = repo, - dispatchers = dispatchers - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt index cb83d2e209..339308eeb9 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt @@ -23,7 +23,12 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.collections.AddObjectToCollection import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.cover.SetDocCoverImage +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewer import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject +import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.DuplicateDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.RenameDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.SetDataViewViewerPosition import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer import com.anytypeio.anytype.domain.event.interactor.EventChannel import com.anytypeio.anytype.domain.event.interactor.InterceptEvents @@ -81,6 +86,8 @@ import com.anytypeio.anytype.presentation.sets.state.ObjectState import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubscription +import com.anytypeio.anytype.presentation.sets.viewer.DefaultViewerDelegate +import com.anytypeio.anytype.presentation.sets.viewer.ViewerDelegate import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory import com.anytypeio.anytype.presentation.util.DefaultCopyFileToCacheDirectory @@ -110,13 +117,10 @@ interface ObjectSetSubComponent { fun objectSetRecordComponent(): ObjectSetRecordSubComponent.Builder fun objectSetCreateBookmarkRecordComponent(): ObjectSetCreateBookmarkRecordSubComponent.Builder fun viewerFilterBySubComponent(): ViewerFilterSubComponent.Builder - fun createDataViewViewerSubComponent(): CreateDataViewViewerSubComponent.Builder - fun editDataViewViewerComponent(): EditDataViewViewerSubComponent.Builder fun dataViewObjectRelationValueComponent(): DataViewObjectRelationValueSubComponent.Builder fun setOrCollectionRelationValueComponent() : SetOrCollectionRelationValueSubComponent.Builder - fun manageViewerComponent(): ManageViewerSubComponent.Builder fun objectSetSettingsComponent(): ObjectSetSettingsSubComponent.Builder fun viewerCardSizeSelectComponent(): ViewerCardSizeSelectSubcomponent.Builder fun viewerImagePreviewSelectComponent(): ViewerImagePreviewSelectSubcomponent.Builder @@ -214,7 +218,8 @@ object ObjectSetModule { updateDataViewViewer: UpdateDataViewViewer, duplicateObjects: DuplicateObjects, templatesContainer: ObjectTypeTemplatesContainer, - setObjectListIsArchived: SetObjectListIsArchived + setObjectListIsArchived: SetObjectListIsArchived, + viewerDelegate: ViewerDelegate ): ObjectSetViewModelFactory = ObjectSetViewModelFactory( openObjectSet = openObjectSet, closeBlock = closeBlock, @@ -249,7 +254,8 @@ object ObjectSetModule { updateDataViewViewer = updateDataViewViewer, duplicateObjects = duplicateObjects, templatesContainer = templatesContainer, - setObjectListIsArchived = setObjectListIsArchived + setObjectListIsArchived = setObjectListIsArchived, + viewerDelegate = viewerDelegate ) @JvmStatic @@ -630,4 +636,67 @@ object ObjectSetModule { defaultProvider: DefaultCoverImageHashProvider ): CoverImageHashProvider } + + @JvmStatic + @Provides + @PerScreen + fun provideAddDataViewViewerUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): AddDataViewViewer = AddDataViewViewer(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideRenameDataViewViewerUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): RenameDataViewViewer = RenameDataViewViewer(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideDuplicateDataViewViewerUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): DuplicateDataViewViewer = DuplicateDataViewViewer(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideDeleteDataViewViewerUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): DeleteDataViewViewer = DeleteDataViewViewer(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideSetDataViewViewerPositionUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetDataViewViewerPosition = SetDataViewViewerPosition(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideViewerDelegate( + session: ObjectSetSession, + addDataViewViewer: AddDataViewViewer, + renameDataViewViewer: RenameDataViewViewer, + duplicateDataViewViewer: DuplicateDataViewViewer, + deleteDataViewViewer: DeleteDataViewViewer, + setDataViewViewerPosition: SetDataViewViewerPosition, + analytics: Analytics, + dispatcher: Dispatcher + ): ViewerDelegate = DefaultViewerDelegate( + session = session, + addDataViewViewer = addDataViewViewer, + renameDataViewViewer = renameDataViewViewer, + duplicateDataViewViewer = duplicateDataViewViewer, + deleteDataViewViewer = deleteDataViewViewer, + setDataViewViewerPosition = setDataViewViewerPosition, + analytics = analytics, + dispatcher = dispatcher + ) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt index 853ed5a479..75e5208888 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt @@ -52,6 +52,7 @@ import com.anytypeio.anytype.core_ui.views.ButtonPrimarySmallIcon import com.anytypeio.anytype.core_ui.widgets.FeaturedRelationGroupWidget import com.anytypeio.anytype.core_ui.widgets.ObjectTypeTemplatesWidget import com.anytypeio.anytype.core_ui.widgets.StatusBadgeWidget +import com.anytypeio.anytype.core_ui.widgets.dv.ViewersWidget import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_ui.widgets.toolbar.DataViewInfo import com.anytypeio.anytype.core_utils.OnSwipeListener @@ -73,6 +74,7 @@ import com.anytypeio.anytype.databinding.FragmentObjectSetBinding import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.presentation.editor.cover.CoverColor import com.anytypeio.anytype.presentation.editor.cover.CoverGradient +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi import com.anytypeio.anytype.presentation.sets.DataViewViewState import com.anytypeio.anytype.presentation.sets.ObjectSetCommand import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel @@ -93,9 +95,6 @@ import com.anytypeio.anytype.ui.relations.RelationTextValueFragment import com.anytypeio.anytype.ui.relations.RelationTextValueFragment.TextValueEditReceiver import com.anytypeio.anytype.ui.relations.RelationValueBaseFragment import com.anytypeio.anytype.ui.relations.RelationValueDVFragment -import com.anytypeio.anytype.ui.sets.modals.CreateDataViewViewerFragment -import com.anytypeio.anytype.ui.sets.modals.EditDataViewViewerFragment -import com.anytypeio.anytype.ui.sets.modals.ManageViewerFragment import com.anytypeio.anytype.ui.sets.modals.ObjectSetSettingsFragment import com.anytypeio.anytype.ui.sets.modals.SetObjectCreateRecordFragmentBase import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment @@ -340,6 +339,16 @@ open class ObjectSetFragment : } observeSelectingTemplate() + + binding.viewersWidget.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + ViewersWidget( + state = vm.viewersWidgetState.collectAsStateWithLifecycle().value, + action = vm::onViewersWidgetAction + ) + } + } } private fun setupWindowInsetAnimation() { @@ -877,24 +886,6 @@ open class ObjectSetFragment : ) ) } - is ObjectSetCommand.Modal.CreateViewer -> { - val fr = CreateDataViewViewerFragment.new( - ctx = command.ctx, - target = command.target - ) - fr.showChildFragment(EMPTY_TAG) - } - is ObjectSetCommand.Modal.EditDataViewViewer -> { - val fr = EditDataViewViewerFragment.new( - ctx = command.ctx, - viewer = command.viewer - ) - fr.showChildFragment(EMPTY_TAG) - } - is ObjectSetCommand.Modal.ManageViewer -> { - val fr = ManageViewerFragment.new(ctx = command.ctx, dv = command.dataview) - fr.showChildFragment(EMPTY_TAG) - } is ObjectSetCommand.Modal.OpenSettings -> { val fr = ObjectSetSettingsFragment.new( ctx = command.ctx, @@ -1109,6 +1100,9 @@ open class ObjectSetFragment : vm.templatesWidgetState.value.showWidget -> { vm.onDismissTemplatesWidget() } + vm.viewersWidgetState.value.showWidget -> { + vm.onViewersWidgetAction(ViewersWidgetUi.Action.Dismiss) + } else -> { vm.onSystemBackPressed() } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt deleted file mode 100644 index 682944e089..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.anytypeio.anytype.ui.sets.modals - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.anytypeio.anytype.core_ui.reactive.clicks -import com.anytypeio.anytype.core_utils.ext.* -import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.databinding.FragmentCreateDataViewViewerBinding -import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.sets.CreateDataViewViewerViewModel -import javax.inject.Inject - -class CreateDataViewViewerFragment : BaseBottomSheetFragment() { - - val ctx get() = arg(CTX_KEY) - val target get() = arg(TARGET_KEY) - - @Inject - lateinit var factory: CreateDataViewViewerViewModel.Factory - private val vm: CreateDataViewViewerViewModel by viewModels { factory } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(lifecycleScope) { - subscribe(binding.btnCreateViewer.clicks()) { - vm.onAddViewer( - name = binding.viewerNameInput.text.toString(), - ctx = ctx, - target = target - ) - } - subscribe(binding.gridContainer.clicks()) { vm.onGridClicked() } - subscribe(binding.galleryContainer.clicks()) { vm.onGalleryClicked() } - subscribe(binding.listContainer.clicks()) { vm.onListClicked() } - } - } - - override fun onStart() { - with(lifecycleScope) { - jobs += subscribe(vm.state) { render(it) } - } - super.onStart() - } - - private fun render(state: CreateDataViewViewerViewModel.ViewState) { - when (state) { - CreateDataViewViewerViewModel.ViewState.Init -> { - binding.isListChosen.invisible() - binding.isTableChosen.visible() - binding.isGalleryChosen.invisible() - } - CreateDataViewViewerViewModel.ViewState.Completed -> { - dismiss() - } - is CreateDataViewViewerViewModel.ViewState.Error -> { - toast(state.msg) - } - CreateDataViewViewerViewModel.ViewState.Gallery -> { - binding.isListChosen.invisible() - binding.isTableChosen.invisible() - binding.isGalleryChosen.visible() - } - CreateDataViewViewerViewModel.ViewState.Grid -> { - binding.isListChosen.invisible() - binding.isTableChosen.visible() - binding.isGalleryChosen.invisible() - } - - CreateDataViewViewerViewModel.ViewState.List -> { - binding.isListChosen.visible() - binding.isTableChosen.invisible() - binding.isGalleryChosen.invisible() - } - } - } - - override fun injectDependencies() { - componentManager().createDataViewViewerComponent.get(ctx).inject(this) - } - - override fun releaseDependencies() { - componentManager().createDataViewViewerComponent.release(ctx) - } - - override fun inflateBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentCreateDataViewViewerBinding = FragmentCreateDataViewViewerBinding.inflate( - inflater, container, false - ) - - companion object { - fun new(ctx: String, target: String) = CreateDataViewViewerFragment().apply { - arguments = bundleOf( - CTX_KEY to ctx, TARGET_KEY to target - ) - } - - private const val CTX_KEY = "arg.create-data-view-viewer.context" - private const val TARGET_KEY = "arg.create-data-view-viewer.target" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt deleted file mode 100644 index cc8c31e45d..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.anytypeio.anytype.ui.sets.modals - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_ui.menu.DataViewEditViewPopupMenu -import com.anytypeio.anytype.core_ui.reactive.clicks -import com.anytypeio.anytype.core_ui.reactive.textChanges -import com.anytypeio.anytype.core_utils.ext.arg -import com.anytypeio.anytype.core_utils.ext.gone -import com.anytypeio.anytype.core_utils.ext.hideKeyboard -import com.anytypeio.anytype.core_utils.ext.invisible -import com.anytypeio.anytype.core_utils.ext.subscribe -import com.anytypeio.anytype.core_utils.ext.toast -import com.anytypeio.anytype.core_utils.ext.visible -import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.databinding.FragmentEditDataViewViewerBinding -import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.sets.EditDataViewViewerViewModel -import javax.inject.Inject - -class -EditDataViewViewerFragment : BaseBottomSheetFragment() { - - private val ctx: Id get() = arg(CTX_KEY) - private val viewer: Id get() = arg(VIEWER_KEY) - - @Inject - lateinit var factory: EditDataViewViewerViewModel.Factory - private val vm: EditDataViewViewerViewModel by viewModels { factory } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(lifecycleScope) { - subscribe(binding.viewerNameInput.textChanges()) { name -> - vm.onViewerNameChanged(name = name.toString()) - } - subscribe(binding.btnDone.clicks()) { - vm.onDoneClicked(ctx, viewer) - } - subscribe(binding.threeDotsButton.clicks()) { vm.onMenuClicked(viewer) } - subscribe(binding.gridContainer.clicks()) { vm.onGridClicked() } - subscribe(binding.galleryContainer.clicks()) { vm.onGalleryClicked() } - subscribe(binding.listContainer.clicks()) { vm.onListClicked() } - } - } - - override fun onStart() { - with(lifecycleScope) { - jobs += subscribe(vm.viewState) { render(it) } - jobs += subscribe(vm.isDismissed) { isDismissed -> - if (isDismissed) { - binding.viewerNameInput.apply { - clearFocus() - hideKeyboard() - } - dismiss() - } - } - jobs += subscribe(vm.isLoading) { isLoading -> - if (isLoading) binding.progressBar.visible() else binding.progressBar.gone() - } - jobs += subscribe(vm.toasts) { toast(it) } - jobs += subscribe(vm.popupCommands) { cmd -> - DataViewEditViewPopupMenu( - requireContext(), - binding.threeDotsButton, - cmd.isDeletionAllowed - ).apply { - setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.duplicate -> vm.onDuplicateClicked(ctx = ctx, viewer = viewer) - R.id.delete -> vm.onDeleteClicked(ctx = ctx, viewer = viewer) - } - true - } - }.show() - } - } - super.onStart() - vm.onStart(viewer) - } - - private fun render(state: EditDataViewViewerViewModel.ViewState) { - when (state) { - EditDataViewViewerViewModel.ViewState.Init -> { - with(binding) { - viewerNameInput.text = null - isListChosen.invisible() - isTableChosen.invisible() - isGalleryChosen.invisible() - } - } - is EditDataViewViewerViewModel.ViewState.Name -> { - binding.viewerNameInput.setText(state.name) - } - EditDataViewViewerViewModel.ViewState.Completed -> { - dismiss() - } - is EditDataViewViewerViewModel.ViewState.Error -> { - toast(state.msg) - } - EditDataViewViewerViewModel.ViewState.Gallery -> { - with(binding) { - isListChosen.invisible() - isTableChosen.invisible() - isGalleryChosen.visible() - } - } - EditDataViewViewerViewModel.ViewState.Grid -> { - with(binding) { - isListChosen.invisible() - isTableChosen.visible() - isGalleryChosen.invisible() - } - } - - EditDataViewViewerViewModel.ViewState.List -> { - with(binding) { - isListChosen.visible() - isTableChosen.invisible() - isGalleryChosen.invisible() - } - } - EditDataViewViewerViewModel.ViewState.Kanban -> {} - } - } - - override fun injectDependencies() { - componentManager().editDataViewViewerComponent.get(ctx).inject(this) - } - - override fun releaseDependencies() { - componentManager().editDataViewViewerComponent.release(ctx) - } - - override fun inflateBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentEditDataViewViewerBinding = FragmentEditDataViewViewerBinding.inflate( - inflater, container, false - ) - - companion object { - const val CTX_KEY = "arg.edit-data-view-viewer.ctx" - const val VIEWER_KEY = "arg.edit-data-view-viewer.viewer" - - fun new( - ctx: Id, - viewer: Id - ): EditDataViewViewerFragment = EditDataViewViewerFragment().apply { - arguments = bundleOf( - CTX_KEY to ctx, - VIEWER_KEY to viewer - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt deleted file mode 100644 index 4f8af0b261..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.anytypeio.anytype.ui.sets.modals - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_ui.features.sets.ManageViewerDoneAdapter -import com.anytypeio.anytype.core_ui.features.sets.ManageViewerEditAdapter -import com.anytypeio.anytype.core_ui.reactive.clicks -import com.anytypeio.anytype.core_ui.tools.DefaultDragAndDropBehavior -import com.anytypeio.anytype.core_utils.ext.* -import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.core_utils.ui.OnStartDragListener -import com.anytypeio.anytype.databinding.FragmentManageViewerBinding -import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -class ManageViewerFragment : BaseBottomSheetFragment(), - OnStartDragListener { - - private val manageViewerAdapter by lazy { - ManageViewerDoneAdapter( - onViewerClicked = { view -> vm.onViewerClicked(ctx = ctx, view = view) } - ) - } - - private val manageViewerEditAdapter by lazy { - ManageViewerEditAdapter( - onDragListener = this, - onButtonMoreClicked = vm::onViewerActionClicked, - onDeleteView = { - vm.onDeleteView( - ctx = ctx, - dv = dv, - view = it - ) - }, - onDeleteActiveView = { toast(R.string.toast_active_view_delete) } - ) - } - - private val ctx: Id get() = arg(CTX_KEY) - private val dv: Id get() = arg(DATA_VIEW_KEY) - - @Inject - lateinit var factory: ManageViewerViewModel.Factory - - private val vm: ManageViewerViewModel by viewModels { factory } - - private val dndItemTouchHelper: ItemTouchHelper by lazy { ItemTouchHelper(dndBehavior) } - private val dndBehavior by lazy { - DefaultDragAndDropBehavior( - onItemMoved = { from, to -> manageViewerEditAdapter.onItemMove(from, to) }, - onItemDropped = { newPosition -> - vm.onOrderChanged( - ctx = ctx, - dv = dv, - newOrder = manageViewerEditAdapter.order, - newPosition = newPosition - ) - } - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.dataViewViewerRecycler.apply { - layoutManager = LinearLayoutManager(context) - } - with(lifecycleScope) { - subscribe(binding.btnEditViewers.clicks()) { vm.onButtonEditClicked() } - subscribe(binding.btnAddNewViewer.clicks()) { vm.onButtonAddClicked() } - } - } - - override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { - } - - override fun onStart() { - lifecycleScope.launch { - jobs += subscribe(vm.commands) { observe(it) } - jobs += subscribe(vm.toasts) { toast(it) } - jobs += subscribe(vm.views) { - manageViewerAdapter.update(it) - manageViewerEditAdapter.update(it) - } - jobs += subscribe(vm.isDismissed) { isDismissed -> - if (isDismissed) dismiss() - } - jobs += subscribe(vm.isEditEnabled) { isEditEnabled -> - if (isEditEnabled) { - with(binding) { - btnEditViewers.setText(R.string.done) - btnAddNewViewer.invisible() - dataViewViewerRecycler.apply { - adapter = manageViewerEditAdapter - dndItemTouchHelper.attachToRecyclerView(this) - - } - } - } else { - with(binding) { - btnEditViewers.setText(R.string.edit) - btnAddNewViewer.visible() - dataViewViewerRecycler.apply { - adapter = manageViewerAdapter - dndItemTouchHelper.attachToRecyclerView(null) - } - } - } - } - } - super.onStart() - } - - private fun observe(command: ManageViewerViewModel.Command) { - when (command) { - is ManageViewerViewModel.Command.OpenEditScreen -> { - val dialog = EditDataViewViewerFragment.new( - ctx = ctx, - viewer = command.id - ) - dialog.show(parentFragmentManager, null) - } - ManageViewerViewModel.Command.OpenCreateScreen -> { - val dialog = CreateDataViewViewerFragment.new( - ctx = ctx, - target = dv - ) - dialog.show(parentFragmentManager, null) - } - } - } - - override fun injectDependencies() { - componentManager().manageViewerComponent.get(ctx).inject(this) - } - - override fun releaseDependencies() { - componentManager().manageViewerComponent.release(ctx) - } - - override fun inflateBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentManageViewerBinding = FragmentManageViewerBinding.inflate( - inflater, container, false - ) - - companion object { - fun new(ctx: Id, dv: Id): ManageViewerFragment = ManageViewerFragment().apply { - arguments = bundleOf(CTX_KEY to ctx, DATA_VIEW_KEY to dv) - } - - const val CTX_KEY = "arg.manage-data-view-viewer.ctx" - const val DATA_VIEW_KEY = "arg.manage-data-view-viewer.dataview" - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_data_view_viewer.xml b/app/src/main/res/layout/fragment_create_data_view_viewer.xml deleted file mode 100644 index 4ac472e395..0000000000 --- a/app/src/main/res/layout/fragment_create_data_view_viewer.xml +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_data_view_viewer.xml b/app/src/main/res/layout/fragment_edit_data_view_viewer.xml deleted file mode 100644 index 1ea676f4da..0000000000 --- a/app/src/main/res/layout/fragment_edit_data_view_viewer.xml +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manage_viewer.xml b/app/src/main/res/layout/fragment_manage_viewer.xml deleted file mode 100644 index b9dc505353..0000000000 --- a/app/src/main/res/layout/fragment_manage_viewer.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_object_set.xml b/app/src/main/res/layout/fragment_object_set.xml index 1be7d77918..835a705df7 100644 --- a/app/src/main/res/layout/fragment_object_set.xml +++ b/app/src/main/res/layout/fragment_object_set.xml @@ -156,4 +156,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> + + \ No newline at end of file diff --git a/core-ui/build.gradle b/core-ui/build.gradle index 883800d608..7533fca182 100644 --- a/core-ui/build.gradle +++ b/core-ui/build.gradle @@ -53,6 +53,8 @@ dependencies { implementation libs.composeToolingPreview debugImplementation libs.composeTooling implementation libs.coilCompose + implementation libs.composeConstraintLayout + implementation libs.composeReorderable testImplementation libs.fragmentTesting testImplementation project(':test:android-utils') diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ManageViewerEditAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ManageViewerEditAdapter.kt deleted file mode 100644 index 734b8c9229..0000000000 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ManageViewerEditAdapter.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.anytypeio.anytype.core_ui.features.sets - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.anytypeio.anytype.core_ui.R -import com.anytypeio.anytype.core_ui.common.AbstractAdapter -import com.anytypeio.anytype.core_ui.common.AbstractViewHolder -import com.anytypeio.anytype.core_ui.databinding.ItemDvManageViewerBinding -import com.anytypeio.anytype.core_ui.databinding.ItemDvManageViewerDoneBinding -import com.anytypeio.anytype.core_ui.tools.SupportDragAndDropBehavior -import com.anytypeio.anytype.core_utils.ext.gone -import com.anytypeio.anytype.core_utils.ext.invisible -import com.anytypeio.anytype.core_utils.ext.shift -import com.anytypeio.anytype.core_utils.ext.toast -import com.anytypeio.anytype.core_utils.ext.visible -import com.anytypeio.anytype.core_utils.ui.ItemTouchHelperViewHolder -import com.anytypeio.anytype.core_utils.ui.OnStartDragListener -import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel.ViewerView - -class ManageViewerEditAdapter( - private val onDragListener: OnStartDragListener, - private val onButtonMoreClicked: (ViewerView) -> Unit, - private val onDeleteView: (ViewerView) -> Unit, - private val onDeleteActiveView: () -> Unit -) : AbstractAdapter(emptyList()), SupportDragAndDropBehavior { - - val order: List get() = items.map { it.id } - - @SuppressLint("ClickableViewAccessibility") - override fun onCreateViewHolder( - parent: ViewGroup, - iewType: Int - ): ViewHolder = ViewHolder( - binding = ItemDvManageViewerBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ).apply { - binding.dndDragger.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) onDragListener.onStartDrag(this) - false - } - binding.icRemove.setOnClickListener { - val pos = bindingAdapterPosition - if (pos != RecyclerView.NO_POSITION) { - onDeleteView(items[pos]) - } - } - binding.btnActionMore.setOnClickListener { - val pos = bindingAdapterPosition - if (pos != RecyclerView.NO_POSITION) { - onButtonMoreClicked(items[pos]) - } - } - binding.icRemoveInactive.setOnClickListener { - onDeleteActiveView() - } - } - - override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { - val update = ArrayList(items).shift(fromPosition, toPosition) - items = update - notifyItemMoved(fromPosition, toPosition) - return true - } - - class ViewHolder( - val binding: ItemDvManageViewerBinding - ) : AbstractViewHolder(binding.root), ItemTouchHelperViewHolder { - - val untitled = binding.root.context.getString(R.string.untitled) - - override fun bind(item: ViewerView) { - binding.title.text = item.name.ifEmpty { - untitled - } - if (item.isActive) { - binding.icRemove.gone() - binding.icRemoveInactive.visible() - } else { - binding.icRemove.visible() - binding.icRemoveInactive.gone() - } - } - - override fun onItemSelected() { - } - - override fun onItemClear() { - } - } -} - -class ManageViewerDoneAdapter( - private val onViewerClicked: (ViewerView) -> Unit -) : AbstractAdapter(emptyList()) { - - override fun onCreateViewHolder( - parent: ViewGroup, - iewType: Int - ): ViewHolder = ViewHolder( - binding = ItemDvManageViewerDoneBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ).apply { - itemView.setOnClickListener { - onViewerClicked(items[bindingAdapterPosition]) - } - } - - class ViewHolder( - val binding: ItemDvManageViewerDoneBinding - ) : AbstractViewHolder(binding.root) { - - private val title = binding.title - private val icChecked = binding.iconChecked - val untitled = binding.root.context.getString(R.string.untitled) - - override fun bind(item: ViewerView) { - title.text = item.name.ifEmpty { - untitled - } - if (item.isActive) { - icChecked.visible() - } else { - icChecked.invisible() - } - } - } -} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Modifiers.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Modifiers.kt index 1f6f8db0e9..23cf15249a 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Modifiers.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Modifiers.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role +import com.anytypeio.anytype.core_ui.extensions.throttledClick @Composable fun Modifier.noRippleClickable( @@ -23,4 +24,21 @@ fun Modifier.noRippleClickable( onClickLabel = onClickLabel, role = role, onClick = onClick, +) + +@Composable +fun Modifier.noRippleThrottledClickable( + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + indication: Indication? = null, + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onClick: () -> Unit, +) = clickable( + interactionSource = interactionSource, + indication = indication, + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + onClick = throttledClick(onClick), ) \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt index 08343719d2..6ce978b11e 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectTypeTemplatesWidget.kt @@ -750,7 +750,7 @@ private fun TemplateItemRectangles() { } } -private enum class DragStates { +enum class DragStates { VISIBLE, DISMISSED } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewersWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewersWidget.kt new file mode 100644 index 0000000000..50a8744a94 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewersWidget.kt @@ -0,0 +1,350 @@ +package com.anytypeio.anytype.core_ui.widgets.dv + +import android.view.HapticFeedbackConstants +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.views.Caption2Regular +import com.anytypeio.anytype.core_ui.views.HeadlineSubheading +import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi.Action.Delete +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi.Action.Dismiss +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi.Action.DoneMode +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi.Action.Edit +import com.anytypeio.anytype.presentation.sets.ViewersWidgetUi.Action.EditMode +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.detectReorder +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable + + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ViewersWidget( + state: ViewersWidgetUi, + action: (ViewersWidgetUi.Action) -> Unit +) { + val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + + LaunchedEffect(key1 = state, block = { + if (state.showWidget) sheetState.show() else sheetState.hide() + }) + + DisposableEffect( + key1 = (sheetState.targetValue == ModalBottomSheetValue.Hidden + && sheetState.isVisible) + ) { + onDispose { + if (sheetState.currentValue == ModalBottomSheetValue.Hidden) { + action(Dismiss) + } + } + } + + ModalBottomSheetLayout( + sheetState = sheetState, + sheetBackgroundColor = Color.Transparent, + sheetShape = RoundedCornerShape(16.dp), + sheetContent = { + ViewersWidgetContent(state, action) + }, + content = { + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + .noRippleThrottledClickable { action.invoke(Dismiss) } + } + ) +} + +@Composable +private fun ViewersWidgetContent( + state: ViewersWidgetUi, + action: (ViewersWidgetUi.Action) -> Unit +) { + val currentState by rememberUpdatedState(state) + + val views = remember { mutableStateOf(currentState.items) } + views.value = currentState.items + + val isEditing = remember { mutableStateOf(currentState.isEditing) } + isEditing.value = currentState.isEditing + + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .shadow( + elevation = 40.dp, + spotColor = Color(0x40000000), + ambientColor = Color(0x40000000) + ) + .padding(start = 8.dp, end = 8.dp, bottom = 31.dp) + .background( + color = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(size = 16.dp) + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 16.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterStart), + ) { + if (currentState.isEditing) { + ActionText( + text = stringResource(id = R.string.done), + click = { action(DoneMode) } + ) + } else { + ActionText( + text = stringResource(id = R.string.edit), + click = { action(EditMode) } + ) + } + } + Box(modifier = Modifier.align(Alignment.Center)) { + Text( + text = stringResource(R.string.views), + style = Title1, + color = colorResource(R.color.text_primary) + ) + } + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + Image( + modifier = Modifier.padding( + start = 16.dp, + top = 12.dp, + bottom = 12.dp, + end = 16.dp + ), + painter = painterResource(id = R.drawable.ic_default_plus), + contentDescription = null + ) + } + } + + val lazyListState = rememberReorderableLazyListState( + onMove = { from, to -> + views.value = views.value.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }, + onDragEnd = { from, to -> + action( + ViewersWidgetUi.Action.OnMove( + currentViews = views.value, + from = from, + to = to + ) + ) + } + ) + + LazyColumn( + state = lazyListState.listState, + modifier = Modifier + .reorderable(lazyListState) + .fillMaxWidth() + .wrapContentHeight() + ) { + itemsIndexed( + items = views.value, + key = { _, item -> item.id }) { index, view -> + ReorderableItem( + reorderableState = lazyListState, + key = view.id + ) { isDragging -> + val currentItem = LocalView.current + if (isDragging) { + currentItem.isHapticFeedbackEnabled = true + currentItem.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + val alpha = + animateFloatAsState(if (isDragging) 0.8f else 1.0f, label = "") + ConstraintLayout( + modifier = Modifier + .height(52.dp) + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp) + .animateContentSize( + animationSpec = spring( + stiffness = Spring.StiffnessLow + ) + ) + .alpha(alpha.value) + ) { + val (delete, text, edit, dnd, unsupported) = createRefs() + Image( + modifier = Modifier + .noRippleThrottledClickable { + action.invoke(Delete(view.id)) + } + .constrainAs(delete) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + visibility = + if (isEditing.value && !view.isActive) Visibility.Visible else Visibility.Gone + }, + painter = painterResource(id = R.drawable.ic_relation_delete), + contentDescription = "Delete view" + ) + Image( + modifier = Modifier + .detectReorder(lazyListState) + .constrainAs(dnd) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + visibility = + if (isEditing.value) Visibility.Visible else Visibility.Gone + + }, + painter = painterResource(id = R.drawable.ic_dnd), + contentDescription = "Dnd view" + ) + Image( + modifier = Modifier + .noRippleThrottledClickable { + action.invoke(Edit(view.id)) + } + .constrainAs(edit) { + end.linkTo(dnd.start, margin = 16.dp) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + visibility = + if (isEditing.value) Visibility.Visible else Visibility.Gone + }, + painter = painterResource(id = R.drawable.ic_edit_24), + contentDescription = "Edit view" + ) + Text( + modifier = Modifier + .constrainAs(unsupported) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(edit.start) + visibility = + if (!isEditing.value && view.isUnsupported) Visibility.Visible else Visibility.Gone + }, + text = stringResource(id = R.string.unsupported), + color = colorResource(id = R.color.text_secondary), + style = Caption2Regular, + textAlign = TextAlign.Left + ) + Text( + modifier = Modifier + .noRippleThrottledClickable { + if (!isEditing.value) { + action.invoke( + ViewersWidgetUi.Action.SetActive( + view.id + ) + ) + } + } + .constrainAs(text) { + start.linkTo( + delete.end, + margin = 12.dp, + goneMargin = 0.dp + ) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo( + unsupported.start, + margin = 8.dp, + goneMargin = 38.dp + ) + width = Dimension.fillToConstraints + }, + text = view.name, + color = colorResource(id = if (view.isActive) R.color.text_primary else R.color.glyph_active), + style = HeadlineSubheading, + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + if (index != views.value.size - 1) { + Divider() + } + } + } + } + } +} + +@Composable +private fun ActionText(text: String, click: () -> Unit) { + Text( + modifier = Modifier + .padding( + start = 16.dp, + top = 12.dp, + bottom = 12.dp, + end = 16.dp + ) + .noRippleThrottledClickable { click() }, + text = text, + style = BodyCalloutRegular, + color = colorResource(id = R.color.glyph_active), + textAlign = TextAlign.Center + ) +} \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/ic_edit_24.xml b/core-ui/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000000..2ce58ce0e1 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_view_drag_24.xml b/core-ui/src/main/res/drawable/ic_view_drag_24.xml new file mode 100644 index 0000000000..92c39bdac6 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_view_drag_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index 9a805fa3fb..5024f5e976 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -244,6 +244,7 @@ Relation Download View + Views Choose emoji logo_transition @@ -651,5 +652,6 @@ Edit template Duplicate Delete + Unsupported diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewer.kt index b1b3b7933f..733389f232 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewer.kt @@ -1,20 +1,22 @@ package com.anytypeio.anytype.domain.dataview.interactor -import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.core_models.DVViewerType import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor /** * Use-case for adding a new viewer to DV. */ class AddDataViewViewer( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.addDataViewViewer( + override suspend fun doWork(params: Params): Payload { + return repo.addDataViewViewer( ctx = params.ctx, target = params.target, name = params.name, diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DeleteDataViewViewer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DeleteDataViewViewer.kt index a1cde9b4d3..f535a4e21c 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DeleteDataViewViewer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DeleteDataViewViewer.kt @@ -2,7 +2,8 @@ package com.anytypeio.anytype.domain.dataview.interactor import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.base.BaseUseCase +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer.Params @@ -12,11 +13,12 @@ import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer.Par * @see [Params] for details. */ class DeleteDataViewViewer( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.removeDataViewViewer( + override suspend fun doWork(params: Params): Payload { + return repo.removeDataViewViewer( ctx = params.ctx, dataview = params.dataview, viewer = params.viewer diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DuplicateDataViewViewer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DuplicateDataViewViewer.kt index f855a99ead..9a49ba16e3 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DuplicateDataViewViewer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DuplicateDataViewViewer.kt @@ -1,22 +1,24 @@ package com.anytypeio.anytype.domain.dataview.interactor -import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.core_models.DVViewer import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.domain.dataview.interactor.DuplicateDataViewViewer.Params import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor /** * Use-case for duplicating data view's view. * @see [Params] for details. */ class DuplicateDataViewViewer( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.duplicateDataViewViewer( + override suspend fun doWork(params: Params): Payload { + return repo.duplicateDataViewViewer( context = params.context, target = params.target, viewer = params.viewer diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RenameDataViewViewer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RenameDataViewViewer.kt index 633ef13251..d223baacea 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RenameDataViewViewer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RenameDataViewViewer.kt @@ -1,22 +1,24 @@ package com.anytypeio.anytype.domain.dataview.interactor -import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.core_models.DVViewer import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.domain.dataview.interactor.RenameDataViewViewer.Params import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor /** * Use-case for renaming data view's view. * @see [Params] for details. */ class RenameDataViewViewer( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.updateDataViewViewer( + override suspend fun doWork(params: Params): Payload { + return repo.updateDataViewViewer( context = params.context, target = params.target, viewer = params.viewer diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 640b37289f..dc06e72d70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ androidxComposeVersion = '1.4.3' composeKotlinCompilerVersion = '1.3.1' composeMaterial3Version = '1.1.1' composeMaterialVersion = '1.3.1' +composeConstraintLayoutVersion = '1.0.1' activityComposeVersion = '1.7.2' composeReorderableVersion = '0.9.6' @@ -80,6 +81,7 @@ composeAccompanistPagerIndicators = { module = "com.google.accompanist:accompani composeAccompanistThemeAdapter = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanistVersion" } composeAccompanistNavAnimation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanistVersion" } composeReorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "composeReorderableVersion" } +composeConstraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "composeConstraintLayoutVersion" } kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.4.1" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompatVersion" } androidAnnotations = { module = "androidx.annotation:annotation", version.ref = "appcompatVersion" } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateDataViewViewerViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateDataViewViewerViewModel.kt deleted file mode 100644 index 42378b8891..0000000000 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateDataViewViewerViewModel.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.anytypeio.anytype.presentation.sets - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.DVViewerType -import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewer -import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.presentation.extension.ObjectStateAnalyticsEvent -import com.anytypeio.anytype.presentation.extension.logEvent -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -class CreateDataViewViewerViewModel( - private val addDataViewViewer: AddDataViewViewer, - private val dispatcher: Dispatcher, - private val analytics: Analytics, - private val objectState: MutableStateFlow -) : BaseViewModel() { - - val state = MutableStateFlow(ViewState.Init) - private var dvType = DVViewerType.GRID - - fun onAddViewer( - name: String, - ctx: String, - target: String, - ) { - val startTime = System.currentTimeMillis() - viewModelScope.launch { - addDataViewViewer( - AddDataViewViewer.Params( - ctx = ctx, - target = target, - name = name, - type = dvType - ) - ).process( - failure = { error -> - Timber.e(error, ERROR_ADD_NEW_VIEW).also { - state.value = ViewState.Error(ERROR_ADD_NEW_VIEW) - } - }, - success = { - dispatcher.send(it).also { - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.ADD_VIEW, - startTime = startTime, - type = dvType.formattedName - ) - state.value = ViewState.Completed - } - } - ) - } - } - - fun onGridClicked() { - dvType = DVViewerType.GRID - state.value = ViewState.Grid - } - - fun onListClicked() { - dvType = DVViewerType.LIST - state.value = ViewState.List - } - - fun onGalleryClicked() { - dvType = DVViewerType.GALLERY - state.value = ViewState.Gallery - } - - - class Factory( - private val addDataViewViewer: AddDataViewViewer, - private val dispatcher: Dispatcher, - private val analytics: Analytics, - private val objectState: MutableStateFlow - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return CreateDataViewViewerViewModel( - addDataViewViewer = addDataViewViewer, - dispatcher = dispatcher, - analytics = analytics, - objectState = objectState - ) as T - } - } - - companion object { - const val ERROR_ADD_NEW_VIEW = "Error while creating a new data view view" - } - - sealed class ViewState { - object Init : ViewState() - object Completed : ViewState() - object Grid : ViewState() - object Gallery : ViewState() - object List : ViewState() - data class Error(val msg: String) : ViewState() - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/EditDataViewViewerViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/EditDataViewViewerViewModel.kt deleted file mode 100644 index 9826bc1fa2..0000000000 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/EditDataViewViewerViewModel.kt +++ /dev/null @@ -1,286 +0,0 @@ -package com.anytypeio.anytype.presentation.sets - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.* -import com.anytypeio.anytype.domain.dataview.interactor.* -import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.presentation.extension.ObjectStateAnalyticsEvent -import com.anytypeio.anytype.presentation.extension.logEvent -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -class EditDataViewViewerViewModel( - private val renameDataViewViewer: RenameDataViewViewer, - private val deleteDataViewViewer: DeleteDataViewViewer, - private val duplicateDataViewViewer: DuplicateDataViewViewer, - private val updateDataViewViewer: UpdateDataViewViewer, - private val dispatcher: Dispatcher, - private val objectState: StateFlow, - private val objectSetSession: ObjectSetSession, - private val paginator: ObjectSetPaginator, - private val analytics: Analytics -) : BaseViewModel() { - - val viewState = MutableStateFlow(ViewState.Init) - val isDismissed = MutableSharedFlow(replay = 0) - val isLoading = MutableStateFlow(false) - val popupCommands = MutableSharedFlow(replay = 0) - - var initialName: String = "" - var initialType: DVViewerType = DVViewerType.GRID - - private var viewerType: DVViewerType = DVViewerType.GRID - private var viewerName: String = "" - - fun onStart(viewerId: Id) { - val state = objectState.value.dataViewState() ?: return - val viewer = state.viewers.firstOrNull { it.id == viewerId } - if (viewer != null) { - initialName = viewer.name - initialType = viewer.type - viewState.value = ViewState.Name(viewer.name) - updateViewState(viewer.type) - } else { - Timber.e("Can't find viewer by id : $viewerId") - } - } - - fun onViewerNameChanged(name: String) { - viewerName = name - } - - fun onDuplicateClicked(ctx: Id, viewer: Id) { - val startTime = System.currentTimeMillis() - val state = objectState.value.dataViewState() ?: return - viewModelScope.launch { - duplicateDataViewViewer( - DuplicateDataViewViewer.Params( - context = ctx, - target = state.dataViewBlock.id, - viewer = state.viewers.first { it.id == viewer } - ) - ).process( - failure = { e -> - Timber.e(e, "Error while duplicating viewer: $viewer") - _toasts.emit("Error while deleting viewer: ${e.localizedMessage}") - }, - success = { - dispatcher.send(it).also { - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.DUPLICATE_VIEW, - startTime = startTime, - type = viewerType.formattedName - ) - isDismissed.emit(true) - } - } - ) - } - } - - fun onDeleteClicked(ctx: Id, viewer: Id) { - val state = objectState.value.dataViewState() ?: return - if (state.viewers.size > 1) { - val targetIdx = state.viewers.indexOfFirst { it.id == viewer } - val isActive = if (objectSetSession.currentViewerId.value != null) { - objectSetSession.currentViewerId.value == viewer - } else { - targetIdx == 0 - } - var nextViewerId: Id? = null - if (isActive) { - nextViewerId = if (targetIdx != state.viewers.lastIndex) - state.viewers[targetIdx.inc()].id - else - state.viewers[targetIdx.dec()].id - } - proceedWithDeletion( - ctx = ctx, - dv = state.dataViewBlock.id, - viewer = viewer, - nextViewerId = nextViewerId - ) - } else { - viewModelScope.launch { - _toasts.emit("Data view should have at least one view") - } - } - } - - private fun proceedWithDeletion( - ctx: Id, - dv: Id, - viewer: Id, - nextViewerId: Id? - ) { - val startTime = System.currentTimeMillis() - viewModelScope.launch { - deleteDataViewViewer( - DeleteDataViewViewer.Params( - ctx = ctx, - viewer = viewer, - dataview = dv - ) - ).process( - failure = { e -> - Timber.e(e, "Error while deleting viewer: $viewer") - _toasts.emit("Error while deleting viewer: ${e.localizedMessage}") - }, - success = { firstPayload -> - dispatcher.send(firstPayload) - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.REMOVE_VIEW, - startTime = startTime - ) - if (nextViewerId != null) { - objectSetSession.currentViewerId.value = nextViewerId - paginator.offset.value = 0 - } - isDismissed.emit(true) - } - ) - } - } - - fun onMenuClicked(viewer: Id) { - val isDeletionAllowed = isDeletionAllowed(viewer) - viewModelScope.launch { - popupCommands.emit(PopupMenuCommand(isDeletionAllowed = isDeletionAllowed)) - } - } - - private fun isDeletionAllowed(viewerId: Id): Boolean { - val activeViewerId = objectSetSession.currentViewerId.value - ?: (objectState.value.dataViewState()?.viewers?.firstOrNull()?.id ?: false) - return viewerId != activeViewerId - } - - fun onDoneClicked(ctx: Id, viewerId: Id) { - Timber.d("onDoneClicked, ctx:[$ctx], viewerId:[$viewerId], viewerType:[${viewerType.formattedName}], viewerName:[$viewerName]") - if (initialName != viewerName || initialType != viewerType) { - updateDVViewerType(ctx, viewerId, viewerType, viewerName) - } else { - viewModelScope.launch { isDismissed.emit(true) } - } - } - - fun onGridClicked() { - updateViewState(DVViewerType.GRID) - } - - fun onListClicked() { - updateViewState(DVViewerType.LIST) - } - - fun onGalleryClicked() { - updateViewState(DVViewerType.GALLERY) - } - - private fun updateDVViewerType(ctx: Id, viewerId: Id, type: DVViewerType, name: String) { - val startTime = System.currentTimeMillis() - val state = objectState.value.dataViewState() ?: return - val viewer = state.viewers.find { it.id == viewerId } - if (viewer != null) { - viewModelScope.launch { - isLoading.value = true - updateDataViewViewer( - UpdateDataViewViewer.Params.Fields( - context = ctx, - target = state.dataViewBlock.id, - viewer = viewer.copy(type = type, name = name) - ) - ).process( - success = { payload -> - dispatcher.send(payload).also { - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.CHANGE_VIEW_TYPE, - startTime = startTime, - type = type.formattedName - ) - isLoading.value = false - isDismissed.emit(true) - } - }, - failure = { - isLoading.value = false - Timber.e(it, "Error while updating Viewer type") - isDismissed.emit(true) - } - ) - } - } else { - sendToast("View not found. Please, try again later.") - } - } - - private fun updateViewState(type: DVViewerType) { - viewerType = type - viewState.value = when (type) { - Block.Content.DataView.Viewer.Type.GRID -> ViewState.Grid - Block.Content.DataView.Viewer.Type.LIST -> ViewState.List - Block.Content.DataView.Viewer.Type.GALLERY -> ViewState.Gallery - Block.Content.DataView.Viewer.Type.BOARD -> ViewState.Kanban - } - } - - class Factory( - private val renameDataViewViewer: RenameDataViewViewer, - private val deleteDataViewViewer: DeleteDataViewViewer, - private val duplicateDataViewViewer: DuplicateDataViewViewer, - private val updateDataViewViewer: UpdateDataViewViewer, - private val dispatcher: Dispatcher, - private val objectState: StateFlow, - private val objectSetSession: ObjectSetSession, - private val paginator: ObjectSetPaginator, - private val analytics: Analytics - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return EditDataViewViewerViewModel( - renameDataViewViewer = renameDataViewViewer, - deleteDataViewViewer = deleteDataViewViewer, - duplicateDataViewViewer = duplicateDataViewViewer, - updateDataViewViewer = updateDataViewViewer, - dispatcher = dispatcher, - objectState = objectState, - objectSetSession = objectSetSession, - paginator = paginator, - analytics = analytics - ) as T - } - } - - private data class ViewerNameUpdate( - val ctx: Id, - val dataview: Id, - val viewer: DVViewer, - val name: String - ) - - data class PopupMenuCommand(val isDeletionAllowed: Boolean = false) - - sealed class ViewState { - object Init : ViewState() - data class Name(val name: String) : ViewState() - object Completed : ViewState() - object Grid : ViewState() - object Gallery : ViewState() - object List : ViewState() - object Kanban : ViewState() - data class Error(val msg: String) : ViewState() - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ManageViewerViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ManageViewerViewModel.kt deleted file mode 100644 index 14db692c0c..0000000000 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ManageViewerViewModel.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.anytypeio.anytype.presentation.sets - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.DVViewerType -import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.base.fold -import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer -import com.anytypeio.anytype.domain.dataview.interactor.SetDataViewViewerPosition -import com.anytypeio.anytype.presentation.common.BaseListViewModel -import com.anytypeio.anytype.presentation.extension.ObjectStateAnalyticsEvent -import com.anytypeio.anytype.presentation.extension.logEvent -import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel.ViewerView -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.Dispatcher -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.launch -import timber.log.Timber - -class ManageViewerViewModel( - private val objectState: StateFlow, - private val session: ObjectSetSession, - private val dispatcher: Dispatcher, - private val analytics: Analytics, - private val deleteDataViewViewer: DeleteDataViewViewer, - private val setDataViewViewerPosition: SetDataViewViewerPosition -) : BaseListViewModel() { - - val isEditEnabled = MutableStateFlow(false) - val isDismissed = MutableSharedFlow(replay = 0) - val commands = MutableSharedFlow(replay = 0) - - init { - viewModelScope.launch { - objectState.filterIsInstance().collect { s -> - _views.value = s.viewers.mapIndexed { index, viewer -> - ViewerView( - id = viewer.id, - name = viewer.name, - type = viewer.type, - isActive = if (session.currentViewerId.value != null) - viewer.id == session.currentViewerId.value - else - index == 0, - showActionMenu = isEditEnabled.value - ) - } - } - } - } - - fun onOrderChanged( - ctx: Id, - dv: Id, - newPosition: Int, - newOrder: List, - ) { - val currentOrder = views.value.map { it.id } - if (newOrder == currentOrder) return - val viewer = newOrder.getOrNull(newPosition) - if (viewer != null) { - viewModelScope.launch { - // Workaround for preserving the previously first view as the active view - val startTime = System.currentTimeMillis() - if (newPosition == 0 && session.currentViewerId.value.isNullOrEmpty()) { - session.currentViewerId.value = views.value.firstOrNull()?.id - } - setDataViewViewerPosition.stream( - params = SetDataViewViewerPosition.Params( - ctx = ctx, - dv = dv, - viewer = viewer, - pos = newPosition - ) - ).collect { result -> - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.REPOSITION_VIEW, - startTime = startTime, - type = views.value.find { it.id == viewer }?.type?.formattedName - ) - result.fold( - onSuccess = { dispatcher.send(it) }, - onFailure = { Timber.e(it, "Error while changing view order") } - ) - } - } - } else { - sendToast("Something went wrong. Please, try again later.") - } - } - - fun onViewerActionClicked(view: ViewerView) { - viewModelScope.launch { - commands.emit(Command.OpenEditScreen(view.id, view.name)) - } - } - - fun onDeleteView( - ctx: Id, - dv: Id, - view: ViewerView - ) { - viewModelScope.launch { - deleteDataViewViewer( - DeleteDataViewViewer.Params( - ctx = ctx, - dataview = dv, - viewer = view.id - ) - ).process( - failure = { Timber.e(it, "Error while deleting view") }, - success = { dispatcher.send(it) } - ) - } - } - - fun onButtonEditClicked() { - isEditEnabled.value = !isEditEnabled.value - _views.value = views.value.map { view -> - view.copy(showActionMenu = isEditEnabled.value) - } - } - - fun onButtonAddClicked() { - viewModelScope.launch { - commands.emit(Command.OpenCreateScreen) - } - } - - fun onViewerClicked( - ctx: Id, - view: ViewerView - ) { - val startTime = System.currentTimeMillis() - if (!isEditEnabled.value) - viewModelScope.launch { - session.currentViewerId.value = view.id - logEvent( - state = objectState.value, - analytics = analytics, - event = ObjectStateAnalyticsEvent.SWITCH_VIEW, - startTime = startTime, - type = view.type.formattedName - ) - isDismissed.emit(true) - } - else - Timber.d("Skipping click in edit mode") - } - - sealed class Command { - data class OpenEditScreen(val id: Id, val name: String) : Command() - object OpenCreateScreen : Command() - } - - class Factory( - private val objectState: StateFlow, - private val session: ObjectSetSession, - private val dispatcher: Dispatcher, - private val analytics: Analytics, - private val deleteDataViewViewer: DeleteDataViewViewer, - private val setDataViewViewerPosition: SetDataViewViewerPosition - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ManageViewerViewModel( - objectState = objectState, - session = session, - dispatcher = dispatcher, - analytics = analytics, - deleteDataViewViewer = deleteDataViewViewer, - setDataViewViewerPosition = setDataViewViewerPosition - ) as T - } - } - - data class ViewerView( - val id: Id, - val name: String, - val type: DVViewerType, - val isActive: Boolean, - val showActionMenu: Boolean = false, - ) -} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetCommand.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetCommand.kt index 5d9272e472..b18a624ee8 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetCommand.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetCommand.kt @@ -13,18 +13,6 @@ sealed class ObjectSetCommand { val isFavorite: Boolean ) : Modal() - data class CreateViewer( - val ctx: String, - val target: Id - ) : Modal() - - data class EditDataViewViewer( - val ctx: Id, - val viewer: Id - ) : Modal() - - data class ManageViewer(val ctx: Id, val dataview: Id) : Modal() - data class OpenSettings( val ctx: Id, val dv: Id, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt index dee0797765..5f3ce01726 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt @@ -1,6 +1,5 @@ package com.anytypeio.anytype.presentation.sets -import android.util.Log import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.CoverType import com.anytypeio.anytype.core_models.DVFilter @@ -8,6 +7,7 @@ import com.anytypeio.anytype.core_models.DVRecord import com.anytypeio.anytype.core_models.DVSort import com.anytypeio.anytype.core_models.DVViewer import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.DVViewerType import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVFilterUpdate import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVSortUpdate import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVViewerFields @@ -44,6 +44,7 @@ import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState +import com.anytypeio.anytype.presentation.sets.viewer.ViewerView import com.anytypeio.anytype.presentation.templates.TemplateView fun ObjectState.DataView.featuredRelations( @@ -479,4 +480,19 @@ fun ObjectWrapper.Type.toTemplateViewBlank(): TemplateView.Blank { typeId = id, layout = recommendedLayout?.code ?: ObjectType.Layout.BASIC.code ) +} + +fun List.toView(session: ObjectSetSession): List { + return mapIndexed { index, viewer -> + ViewerView( + id = viewer.id, + name = viewer.name, + type = viewer.type, + isActive = if (session.currentViewerId.value != null) + viewer.id == session.currentViewerId.value + else + index == 0, + isUnsupported = viewer.type == DVViewerType.BOARD + ) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt index 6cec7e8495..4b344cc9c5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt @@ -68,6 +68,9 @@ import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription +import com.anytypeio.anytype.presentation.sets.viewer.ViewerDelegate +import com.anytypeio.anytype.presentation.sets.viewer.ViewerEvent +import com.anytypeio.anytype.presentation.sets.viewer.ViewerView import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer import com.anytypeio.anytype.presentation.templates.TemplateMenuClick import com.anytypeio.anytype.presentation.templates.TemplateView @@ -131,8 +134,9 @@ class ObjectSetViewModel( private val updateDataViewViewer: UpdateDataViewViewer, private val duplicateObjects: DuplicateObjects, private val templatesContainer: ObjectTypeTemplatesContainer, - private val setObjectListIsArchived: SetObjectListIsArchived -) : ViewModel(), SupportNavigation> { + private val setObjectListIsArchived: SetObjectListIsArchived, + private val viewerDelegate: ViewerDelegate +) : ViewModel(), SupportNavigation>, ViewerDelegate by viewerDelegate { val status = MutableStateFlow(SyncStatus.UNKNOWN) val error = MutableStateFlow(null) @@ -160,6 +164,7 @@ class ObjectSetViewModel( val currentViewer = _currentViewer private val _templateViews = MutableStateFlow>(emptyList()) + private val _dvViews = MutableStateFlow>(emptyList()) private val _header = MutableStateFlow( SetOrCollectionHeaderState.None @@ -168,6 +173,7 @@ class ObjectSetViewModel( val isCustomizeViewPanelVisible = MutableStateFlow(false) val templatesWidgetState = MutableStateFlow(TemplatesWidgetUiState.init()) + val viewersWidgetState = MutableStateFlow(ViewersWidgetUi.init()) @Deprecated("could be deleted") val isLoading = MutableStateFlow(false) @@ -266,6 +272,12 @@ class ObjectSetViewModel( templatesWidgetState.value = templatesWidgetState.value.copy(items = it) } } + + viewModelScope.launch { + _dvViews.collectLatest { + viewersWidgetState.value = viewersWidgetState.value.copy(items = it) + } + } } private suspend fun proceedWithSettingUnsplashImage( @@ -458,7 +470,6 @@ class ObjectSetViewModel( dataViewState = dataViewState, objectState = objectState, currentViewId = currentViewId, - templates = templates ) ObjectState.Init -> DataViewViewState.Init ObjectState.ErrorLayout -> DataViewViewState.Error(msg = "Wrong layout, couldn't open object") @@ -477,6 +488,7 @@ class ObjectSetViewModel( return when (dataViewState) { DataViewState.Init -> { + _dvViews.value = emptyList() if (dvViewer == null) { DataViewViewState.Collection.NoView } else { @@ -484,6 +496,7 @@ class ObjectSetViewModel( } } is DataViewState.Loaded -> { + _dvViews.value = objectState.viewers.toView(session) val relations = objectState.dataViewContent.relationLinks.mapNotNull { storeOfRelations.getByKey(it.key) } @@ -520,7 +533,6 @@ class ObjectSetViewModel( dataViewState: DataViewState, objectState: ObjectState.DataView.Set, currentViewId: String?, - templates: List ): DataViewViewState { if (!objectState.isInitialized) return DataViewViewState.Init @@ -530,6 +542,7 @@ class ObjectSetViewModel( return when (dataViewState) { DataViewState.Init -> { + _dvViews.value = emptyList() when { setOfValue.isEmpty() || query.isEmpty() -> DataViewViewState.Set.NoQuery viewer == null -> DataViewViewState.Set.NoView @@ -537,6 +550,7 @@ class ObjectSetViewModel( } } is DataViewState.Loaded -> { + _dvViews.value = objectState.viewers.toView(session) val relations = objectState.dataViewContent.relationLinks.mapNotNull { storeOfRelations.getByKey(it.key) } @@ -1043,16 +1057,12 @@ class ObjectSetViewModel( fun onExpandViewerMenuClicked() { Timber.d("onExpandViewerMenuClicked, ") - val state = stateReducer.state.value.dataViewState() ?: return if (isRestrictionPresent(DataViewRestriction.VIEWS) ) { toast(NOT_ALLOWED) } else { - dispatch( - ObjectSetCommand.Modal.ManageViewer( - ctx = context, - dataview = state.dataViewBlock.id - ) + viewersWidgetState.value = viewersWidgetState.value.copy( + showWidget = true ) } } @@ -1061,12 +1071,6 @@ class ObjectSetViewModel( Timber.d("onViewerEditClicked, ") val state = stateReducer.state.value.dataViewState() ?: return val viewer = state.viewerById(session.currentViewerId.value) ?: return - dispatch( - ObjectSetCommand.Modal.EditDataViewViewer( - ctx = context, - viewer = viewer.id - ) - ) } fun onMenuClicked() { @@ -1748,6 +1752,66 @@ class ObjectSetViewModel( } //endregion + // region VIEWS + fun onViewersWidgetAction(action: ViewersWidgetUi.Action) { + when (action) { + ViewersWidgetUi.Action.Dismiss -> { + viewersWidgetState.value = viewersWidgetState.value.copy( + showWidget = false, + isEditing = false + ) + } + ViewersWidgetUi.Action.DoneMode -> { + viewersWidgetState.value = viewersWidgetState.value.copy(isEditing = false) + } + ViewersWidgetUi.Action.EditMode -> { + viewersWidgetState.value = viewersWidgetState.value.copy(isEditing = true) + } + is ViewersWidgetUi.Action.Delete -> { + val state = stateReducer.state.value.dataViewState() ?: return + viewModelScope.launch { + onEvent( + ViewerEvent.Delete( + ctx = context, + dv = state.dataViewBlock.id, + viewer = action.viewer, + ) + ) + } + } + is ViewersWidgetUi.Action.Edit -> TODO() + is ViewersWidgetUi.Action.OnMove -> { + Timber.d("onMove Viewer, from:[$action.from], to:[$action.to]") + if (action.from == action.to) return + val state = stateReducer.state.value.dataViewState() ?: return + if (action.to == 0 && session.currentViewerId.value.isNullOrEmpty()) { + session.currentViewerId.value = action.currentViews.firstOrNull()?.id + } + viewModelScope.launch { + viewerDelegate.onEvent( + ViewerEvent.UpdatePosition( + ctx = context, + dv = state.dataViewBlock.id, + viewer = action.currentViews[action.to].id, + position = action.to + ) + ) + } + } + is ViewersWidgetUi.Action.SetActive -> { + viewModelScope.launch { + onEvent(ViewerEvent.SetActive(viewer = action.id)) + } + } + + ViewersWidgetUi.Action.Plus -> { + + } + } + + } + //endregion + companion object { const val NOT_ALLOWED = "Not allowed for this set" const val NOT_ALLOWED_CELL = "Not allowed for this cell" diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt index 81b6e88564..b011a10a5f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt @@ -33,6 +33,7 @@ import com.anytypeio.anytype.presentation.common.Delegator import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription +import com.anytypeio.anytype.presentation.sets.viewer.ViewerDelegate import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer import com.anytypeio.anytype.presentation.util.Dispatcher @@ -70,7 +71,8 @@ class ObjectSetViewModelFactory( private val updateDataViewViewer: UpdateDataViewViewer, private val duplicateObjects: DuplicateObjects, private val templatesContainer: ObjectTypeTemplatesContainer, - private val setObjectListIsArchived: SetObjectListIsArchived + private val setObjectListIsArchived: SetObjectListIsArchived, + private val viewerDelegate: ViewerDelegate ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -108,7 +110,8 @@ class ObjectSetViewModelFactory( updateDataViewViewer = updateDataViewViewer, duplicateObjects = duplicateObjects, templatesContainer = templatesContainer, - setObjectListIsArchived = setObjectListIsArchived + setObjectListIsArchived = setObjectListIsArchived, + viewerDelegate = viewerDelegate ) as T } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewersWidgetUi.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewersWidgetUi.kt new file mode 100644 index 0000000000..03779b1c80 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewersWidgetUi.kt @@ -0,0 +1,45 @@ +package com.anytypeio.anytype.presentation.sets + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.viewer.ViewerView +import com.anytypeio.anytype.presentation.widgets.FromIndex +import com.anytypeio.anytype.presentation.widgets.ToIndex + +data class ViewersWidgetUi( + val showWidget: Boolean, + val isEditing: Boolean, + val items: List +) { + + fun dismiss() = copy( + showWidget = false, + isEditing = false + ) + + companion object { + fun init() = ViewersWidgetUi( + showWidget = false, + isEditing = false, + items = emptyList() + ) + } + + sealed class Action { + object Dismiss : Action() + object EditMode : Action() + object DoneMode : Action() + data class Delete(val viewer: Id) : Action() + data class Edit(val id: Id) : Action() + data class OnMove( + val currentViews: List, + val from: FromIndex, + val to: ToIndex + ) : Action() + + data class SetActive(val id: Id) : Action() + + object Plus : Action() + } +} + + diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerDelegate.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerDelegate.kt new file mode 100644 index 0000000000..12d55bb0ba --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerDelegate.kt @@ -0,0 +1,48 @@ +package com.anytypeio.anytype.presentation.sets.viewer + +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.core_models.DVViewer +import com.anytypeio.anytype.core_models.DVViewerType +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.base.fold +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.DuplicateDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.RenameDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.SetDataViewViewerPosition +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import javax.inject.Inject +import timber.log.Timber + +interface ViewerDelegate { + suspend fun onEvent(event: ViewerEvent) +} + +sealed class ViewerEvent { + data class Delete(val ctx: Id, val dv: Id, val viewer: Id) : ViewerEvent() + data class Duplicate(val ctx: Id, val dv: Id, val viewer: DVViewer) : ViewerEvent() + data class Rename(val ctx: Id, val dv: Id, val viewer: DVViewer) : ViewerEvent() + data class AddNew(val ctx: Id, val dv: Id, val name: String, val type: DVViewerType) : + ViewerEvent() + + data class UpdatePosition(val ctx: Id, val dv: Id, val viewer: Id, val position: Int) : + ViewerEvent() + + data class SetActive(val viewer: Id) : ViewerEvent() +} + +class DefaultViewerDelegate @Inject constructor( + private val session: ObjectSetSession, + private val dispatcher: Dispatcher, + private val analytics: Analytics, + private val deleteDataViewViewer: DeleteDataViewViewer, + private val setDataViewViewerPosition: SetDataViewViewerPosition, + private val duplicateDataViewViewer: DuplicateDataViewViewer, + private val addDataViewViewer: AddDataViewViewer, + private val renameDataViewViewer: RenameDataViewViewer +) : ViewerDelegate { + + override suspend fun onEvent(event: ViewerEvent) {} +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerView.kt new file mode 100644 index 0000000000..ab86f10d99 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/viewer/ViewerView.kt @@ -0,0 +1,15 @@ +package com.anytypeio.anytype.presentation.sets.viewer + +import com.anytypeio.anytype.core_models.DVViewerType +import com.anytypeio.anytype.core_models.Id + +data class ViewerView( + val id: Id, + val name: String, + val type: DVViewerType, + val isActive: Boolean, + val showActionMenu: Boolean = false, + val isUnsupported: Boolean = false +) + + diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt index fa1965c310..49fa500da4 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt @@ -60,6 +60,7 @@ import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubscription import com.anytypeio.anytype.presentation.sets.updateFormatForSubscription +import com.anytypeio.anytype.presentation.sets.viewer.ViewerDelegate import com.anytypeio.anytype.presentation.templates.ObjectTypeTemplatesContainer import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule import com.anytypeio.anytype.presentation.util.Dispatcher @@ -166,6 +167,9 @@ open class ObjectSetViewModelTestSetup { @Mock lateinit var updateDataViewViewer: UpdateDataViewViewer + @Mock + lateinit var viewerDelegate: ViewerDelegate + var stateReducer = DefaultObjectStateReducer() lateinit var dataViewSubscriptionContainer: DataViewSubscriptionContainer @@ -237,7 +241,8 @@ open class ObjectSetViewModelTestSetup { updateDataViewViewer = updateDataViewViewer, templatesContainer = templatesContainer, setObjectListIsArchived = setObjectListIsArchived, - duplicateObjects = duplicateObjects + duplicateObjects = duplicateObjects, + viewerDelegate = viewerDelegate ) } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewerDeleteTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewerDeleteTest.kt deleted file mode 100644 index fa173b9685..0000000000 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewerDeleteTest.kt +++ /dev/null @@ -1,330 +0,0 @@ -package com.anytypeio.anytype.presentation.sets.main - -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.core_models.* -import com.anytypeio.anytype.domain.base.Either -import com.anytypeio.anytype.domain.dataview.interactor.* -import com.anytypeio.anytype.presentation.sets.EditDataViewViewerViewModel -import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator -import com.anytypeio.anytype.presentation.sets.ObjectSetSession -import com.anytypeio.anytype.presentation.sets.state.ObjectState -import com.anytypeio.anytype.presentation.util.CoroutinesTestRule -import com.anytypeio.anytype.presentation.util.Dispatcher -import com.anytypeio.anytype.test_utils.MockDataFactory -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.* - -class ObjectSetViewerDeleteTest { - - @get:Rule - val coroutineTestRule = CoroutinesTestRule() - - val title = Block( - id = MockDataFactory.randomUuid(), - content = Block.Content.Text( - style = Block.Content.Text.Style.TITLE, - text = MockDataFactory.randomString(), - marks = emptyList() - ), - children = emptyList(), - fields = Block.Fields.empty() - ) - - val header = Block( - id = MockDataFactory.randomUuid(), - content = Block.Content.Layout( - type = Block.Content.Layout.Type.HEADER - ), - fields = Block.Fields.empty(), - children = listOf(title.id) - ) - - val paginator = ObjectSetPaginator() - - @Mock - lateinit var dispatcher: Dispatcher - - @Mock - lateinit var renameDataViewViewer: RenameDataViewViewer - - @Mock - lateinit var deleteDataViewViewer: DeleteDataViewViewer - - @Mock - lateinit var duplicateDataViewViewer: DuplicateDataViewViewer - - @Mock - lateinit var updateDataViewViewer: UpdateDataViewViewer - - @Mock - lateinit var analytics: Analytics - - private val ctx: Id = MockDataFactory.randomUuid() - - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - } - - @Test - fun `should not update active view after inactive view is deleted`() = runTest { - // SETUP - - val firstViewer = DVViewer( - id = MockDataFactory.randomUuid(), - name = MockDataFactory.randomString(), - viewerRelations = emptyList(), - type = DVViewerType.GRID, - sorts = emptyList(), - filters = emptyList() - ) - - val secondViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - val thirdViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - - val activeViewerId = firstViewer.id - - val objectSetSession = ObjectSetSession().apply { - currentViewerId.value = activeViewerId - } - - val dv = Block( - id = MockDataFactory.randomUuid(), - content = DV( - viewers = listOf(firstViewer, secondViewer, thirdViewer) - ), - children = emptyList(), - fields = Block.Fields.empty() - ) - - val objectSetState = MutableStateFlow( - ObjectState.DataView.Set( - root = ctx, - blocks = listOf( - header, - title, - dv - ) - ) - ) - - stubRemoveDataViewViewer( - dv = dv.id, - viewer = secondViewer.id - ) - - val vm = buildViewModel( - objectSetState = objectSetState, - objectSetSession = objectSetSession, - updateDataViewViewer = updateDataViewViewer - ) - - // TESTING - - vm.onDeleteClicked( - ctx = ctx, - viewer = secondViewer.id - ) - - verifyBlocking(deleteDataViewViewer, times(1)) { - invoke( - DeleteDataViewViewer.Params( - ctx = ctx, - viewer = secondViewer.id, - dataview = dv.id - ) - ) - } - } - - @Test - fun `should set next view as active view if currently active view is being deleted`() { - - // SETUP - - val firstViewer = DVViewer( - id = MockDataFactory.randomUuid(), - name = MockDataFactory.randomString(), - viewerRelations = emptyList(), - type = DVViewerType.GRID, - sorts = emptyList(), - filters = emptyList() - ) - - val secondViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - val thirdViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - - val activeViewerId = firstViewer.id - - val objectSetSession = ObjectSetSession().apply { - currentViewerId.value = activeViewerId - } - - val dv = Block( - id = MockDataFactory.randomUuid(), - content = DV( - viewers = listOf(firstViewer, secondViewer, thirdViewer) - ), - children = emptyList(), - fields = Block.Fields.empty() - ) - - val objectSetState = MutableStateFlow( - ObjectState.DataView.Set( - root = ctx, - blocks = listOf( - header, - title, - dv - ) - ) - ) - - stubRemoveDataViewViewer( - dv = dv.id, - viewer = firstViewer.id - ) - - val vm = buildViewModel( - objectSetState = objectSetState, - objectSetSession = objectSetSession, - updateDataViewViewer = updateDataViewViewer - ) - - // TESTING - - vm.onDeleteClicked( - ctx = ctx, - viewer = firstViewer.id - ) - - verifyBlocking(deleteDataViewViewer, times(1)) { - invoke( - DeleteDataViewViewer.Params( - ctx = ctx, - viewer = firstViewer.id, - dataview = dv.id - ) - ) - } - } - - @Test - fun `should set prevous view as active view if currently active view is being deleted`() { - - // SETUP - - val firstViewer = DVViewer( - id = MockDataFactory.randomUuid(), - name = MockDataFactory.randomString(), - viewerRelations = emptyList(), - type = DVViewerType.GRID, - sorts = emptyList(), - filters = emptyList() - ) - - val secondViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - val thirdViewer = firstViewer.copy(id = MockDataFactory.randomUuid()) - - val activeViewerId = thirdViewer.id - - val objectSetSession = ObjectSetSession().apply { - currentViewerId.value = activeViewerId - } - - val dv = Block( - id = MockDataFactory.randomUuid(), - content = DV( - viewers = listOf(firstViewer, secondViewer, thirdViewer) - ), - children = emptyList(), - fields = Block.Fields.empty() - ) - - val objectSetState = MutableStateFlow( - ObjectState.DataView.Set( - root = ctx, - blocks = listOf( - header, - title, - dv - ) - ) - ) - - stubRemoveDataViewViewer( - dv = dv.id, - viewer = thirdViewer.id - ) - - val vm = buildViewModel( - objectSetState = objectSetState, - objectSetSession = objectSetSession, - updateDataViewViewer = updateDataViewViewer - ) - - // TESTING - - vm.onDeleteClicked( - ctx = ctx, - viewer = thirdViewer.id - ) - - verifyBlocking(deleteDataViewViewer, times(1)) { - invoke( - DeleteDataViewViewer.Params( - ctx = ctx, - viewer = thirdViewer.id, - dataview = dv.id - ) - ) - } - } - - fun buildViewModel( - updateDataViewViewer: UpdateDataViewViewer, - objectSetState: StateFlow, - objectSetSession: ObjectSetSession - ): EditDataViewViewerViewModel { - return EditDataViewViewerViewModel( - renameDataViewViewer = renameDataViewViewer, - deleteDataViewViewer = deleteDataViewViewer, - duplicateDataViewViewer = duplicateDataViewViewer, - objectSetSession = objectSetSession, - objectState = objectSetState, - dispatcher = dispatcher, - updateDataViewViewer = updateDataViewViewer, - analytics = analytics, - paginator = paginator - ) - } - - private fun stubRemoveDataViewViewer( - viewer: Id, - dv: Id - ) { - deleteDataViewViewer.stub { - onBlocking { - invoke( - DeleteDataViewViewer.Params( - ctx = ctx, - viewer = viewer, - dataview = dv - ) - ) - } doReturn Either.Right( - Payload( - context = ctx, - events = emptyList() - ) - ) - } - } -} \ No newline at end of file