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

DROID-1431 Editor | Enhancement | Blank template (#242)

This commit is contained in:
Konstantin Ivanov 2023-07-29 13:31:14 +02:00 committed by GitHub
parent 9c3801ee13
commit 315f95624d
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 661 additions and 131 deletions

View file

@ -92,6 +92,7 @@ import com.anytypeio.anytype.di.feature.settings.DaggerFilesStorageComponent
import com.anytypeio.anytype.di.feature.settings.LogoutWarningModule
import com.anytypeio.anytype.di.feature.settings.MainSettingsModule
import com.anytypeio.anytype.di.feature.settings.ProfileModule
import com.anytypeio.anytype.di.feature.templates.DaggerTemplateBlankComponent
import com.anytypeio.anytype.di.feature.types.DaggerTypeCreationComponent
import com.anytypeio.anytype.di.feature.types.DaggerTypeEditComponent
import com.anytypeio.anytype.di.feature.types.DaggerTypeIconPickComponent
@ -815,6 +816,10 @@ class ComponentManager(
.build()
}
val templateBlankComponent = Component {
DaggerTemplateBlankComponent.factory().create(findComponentDependencies())
}
// Settings
val aboutAppComponent = Component {

View file

@ -0,0 +1,76 @@
package com.anytypeio.anytype.di.feature.templates
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.templates.TemplateBlankViewModelFactory
import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider
import com.anytypeio.anytype.ui.templates.TemplateBlankFragment
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Scope
@Component(
modules = [TemplateBlankModule::class],
dependencies = [TemplateBlankDependencies::class]
)
@TemplateBlankScope
interface TemplateBlankComponent {
@Component.Factory
interface Factory {
fun create(dependencies: TemplateBlankDependencies): TemplateBlankComponent
}
fun inject(fragment: TemplateBlankFragment)
}
@Module
object TemplateBlankModule {
@JvmStatic
@TemplateBlankScope
@Provides
fun provideToggleHolder(): ToggleStateHolder = ToggleStateHolder.Default()
@JvmStatic
@TemplateBlankScope
@Provides
fun provideCoverImageHashProvider(): CoverImageHashProvider = DefaultCoverImageHashProvider()
@JvmStatic
@TemplateBlankScope
@Provides
fun provideViewModelFactory(
renderer: DefaultBlockViewRenderer
): ViewModelProvider.Factory = TemplateBlankViewModelFactory(
renderer = renderer
)
@Module
interface Declarations {
@TemplateBlankScope
@Binds
fun bindRenderer(
defaultRenderer: DefaultBlockViewRenderer
): DefaultBlockViewRenderer
}
}
interface TemplateBlankDependencies : ComponentDependencies {
fun urlBuilder(): UrlBuilder
fun storeOfRelations(): StoreOfRelations
fun storeOfObjectTypes(): StoreOfObjectTypes
}
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class TemplateBlankScope

View file

@ -5,13 +5,13 @@ import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.presentation.templates.TemplateSelectViewModel
import com.anytypeio.anytype.ui.templates.TemplateSelectFragment
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
import kotlinx.coroutines.Dispatchers
@Subcomponent(
modules = [TemplateSelectModule::class, TemplateSelectModule.Bindings::class]
@ -38,6 +38,15 @@ object TemplateSelectModule {
dispatchers = dispatchers
)
@JvmStatic
@Provides
@PerScreen
fun getTemplates(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): GetTemplates =
GetTemplates(
repo = repo,
dispatchers = dispatchers
)
@Module
interface Bindings {
@PerScreen

View file

@ -37,6 +37,7 @@ import com.anytypeio.anytype.di.feature.settings.FilesStorageDependencies
import com.anytypeio.anytype.di.feature.settings.LogoutWarningSubComponent
import com.anytypeio.anytype.di.feature.settings.MainSettingsSubComponent
import com.anytypeio.anytype.di.feature.settings.ProfileSubComponent
import com.anytypeio.anytype.di.feature.templates.TemplateBlankDependencies
import com.anytypeio.anytype.di.feature.templates.TemplateSelectSubComponent
import com.anytypeio.anytype.di.feature.templates.TemplateSubComponent
import com.anytypeio.anytype.di.feature.types.TypeCreationDependencies
@ -93,7 +94,8 @@ interface MainComponent :
OnboardingSoulCreationDependencies,
OnboardingLoginSetupDependencies,
AboutAppDependencies,
OnboardingSoulCreationAnimDependencies {
OnboardingSoulCreationAnimDependencies,
TemplateBlankDependencies {
fun inject(app: AndroidApplication)
@ -245,5 +247,8 @@ private abstract class ComponentDependenciesModule private constructor() {
@ComponentDependenciesKey(OnboardingSoulCreationAnimDependencies::class)
abstract fun provideOnboardingSoulCreationAnimDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(TemplateBlankDependencies::class)
abstract fun provideTemplateBlankDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -268,13 +268,11 @@ class Navigator : AppNavigation {
override fun openTemplates(
ctx: Id,
type: String,
templates: List<Id>
) {
navController?.navigate(
R.id.templateSelectScreen,
bundleOf(
resId = R.id.templateSelectScreen,
args = bundleOf(
TemplateSelectFragment.CTX_KEY to ctx,
TemplateSelectFragment.TEMPLATE_IDS_KEY to templates,
TemplateSelectFragment.OBJECT_TYPE_KEY to type
)
)

View file

@ -69,8 +69,7 @@ class NavigationRouter(
is AppNavigation.Command.OpenTemplates -> navigation.openTemplates(
ctx = command.ctx,
type = command.type,
templates = command.templates
type = command.type
)
is AppNavigation.Command.OpenLibrary -> navigation.openLibrary()

View file

@ -455,7 +455,7 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
val behavior = BottomSheetBehavior.from(binding.typeHasTemplateToolbar)
when (state) {
is SelectTemplateViewState.Active -> {
binding.typeHasTemplateToolbar.count = state.count
binding.typeHasTemplateToolbar.setText(state.count, state.typeName)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.addBottomSheetCallback(onHideBottomSheetCallback)
}

View file

@ -0,0 +1,143 @@
package com.anytypeio.anytype.ui.templates
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.LinearLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.features.editor.DragAndDropAdapterDelegate
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.argInt
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.databinding.FragmentTemplateBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.templates.TemplateBlankViewModel
import com.anytypeio.anytype.presentation.templates.TemplateBlankViewModelFactory
import java.util.LinkedList
import javax.inject.Inject
import timber.log.Timber
class TemplateBlankFragment : BaseFragment<FragmentTemplateBinding>(R.layout.fragment_template),
ClipboardInterceptor {
@Inject
lateinit var factory: TemplateBlankViewModelFactory
val vm by viewModels<TemplateBlankViewModel> { factory }
private val typeId: String get() = arg(OBJECT_TYPE_ID_KEY)
private val typeName: String get() = arg(OBJECT_TYPE_NAME_KEY)
private val layout: Int get() = argInt(OBJECT_LAYOUT_KEY)
private val templateAdapter by lazy {
BlockAdapter(
restore = LinkedList(),
initialBlock = mutableListOf(),
onTextChanged = { _, _ -> },
onTextBlockTextChanged = {},
onDescriptionChanged = { },
onTitleBlockTextChanged = { _, _ -> },
onSelectionChanged = { _, _ -> },
onCheckboxClicked = {},
onTitleCheckboxClicked = {},
onFocusChanged = { _, _ -> },
onSplitLineEnterClicked = { _, _, _ -> },
onSplitDescription = { _, _, _ -> },
onEmptyBlockBackspaceClicked = {},
onNonEmptyBlockBackspaceClicked = { _, _ -> },
onTextInputClicked = { },
onPageIconClicked = {},
onCoverClicked = { },
onTogglePlaceholderClicked = { },
onToggleClicked = {},
onTitleTextInputClicked = {},
onClickListener = {},
clipboardInterceptor = object : ClipboardInterceptor {
override fun onClipboardAction(action: ClipboardInterceptor.Action) {
TODO("Not yet implemented")
}
override fun onBookmarkPasted(url: Url) {
TODO("Not yet implemented")
}
},
onMentionEvent = {},
onSlashEvent = {},
onBackPressedCallback = { false },
onKeyPressedEvent = {},
onDragAndDropTrigger = { _, _ -> false },
onDragListener = { _, _ -> false },
lifecycle = lifecycle,
dragAndDropSelector = DragAndDropAdapterDelegate(),
onCellSelectionChanged = { _, _ -> }
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.templateRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = templateAdapter
}
}
override fun onStart() {
with(lifecycleScope) {
jobs += subscribe(vm.state) {
Timber.d("TemplateBlankFragment: $it")
templateAdapter.updateWithDiffUtil(it)
}
}
super.onStart()
vm.onStart(typeId, typeName, layout)
}
override fun injectDependencies() {
componentManager().templateBlankComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().templateBlankComponent.release()
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentTemplateBinding = FragmentTemplateBinding.inflate(
inflater, container, false
)
override fun onClipboardAction(action: ClipboardInterceptor.Action) {
// Do nothing
}
override fun onBookmarkPasted(url: Url) {
// Do nothing
}
companion object {
fun new(
typeId: String,
typeName: String,
layout: Int
) = TemplateBlankFragment().apply {
arguments = bundleOf(
OBJECT_TYPE_ID_KEY to typeId,
OBJECT_TYPE_NAME_KEY to typeName,
OBJECT_LAYOUT_KEY to layout
)
}
const val OBJECT_TYPE_ID_KEY = "arg.template.object_type_id"
const val OBJECT_TYPE_NAME_KEY = "arg.template.object_type"
const val OBJECT_LAYOUT_KEY = "arg.template.object_layout"
}
}

View file

@ -0,0 +1,31 @@
package com.anytypeio.anytype.ui.templates
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.anytypeio.anytype.presentation.templates.TemplateSelectViewModel
class TemplateSelectAdapter(
private var items: List<TemplateSelectViewModel.TemplateView>,
fragment: Fragment
) : FragmentStateAdapter(fragment) {
fun update(newItems: List<TemplateSelectViewModel.TemplateView>) {
items = newItems
notifyDataSetChanged()
}
override fun getItemCount(): Int = items.size
override fun createFragment(position: Int): Fragment {
return when (val templateView = items[position]) {
is TemplateSelectViewModel.TemplateView.Blank -> TemplateBlankFragment.new(
typeId = templateView.typeId,
typeName = templateView.typeName,
layout = templateView.layout
)
is TemplateSelectViewModel.TemplateView.Template -> TemplateFragment.new(
templateView.id
)
}
}
}

View file

@ -4,58 +4,92 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.argBoolean
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.databinding.FragmentTemplateSelectBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.templates.TemplateSelectViewModel
import com.anytypeio.anytype.ui.base.NavigationFragment
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
class TemplateSelectFragment :
NavigationFragment<FragmentTemplateSelectBinding>(R.layout.fragment_template_select) {
BaseFragment<FragmentTemplateSelectBinding>(R.layout.fragment_template_select) {
private val vm by viewModels<TemplateSelectViewModel> { factory }
@Inject
lateinit var factory: TemplateSelectViewModel.Factory
private val ids: List<Id> get() = arg(TEMPLATE_IDS_KEY)
private val type: Id get() = arg(OBJECT_TYPE_KEY)
private val ctx: Id get() = arg(CTX_KEY)
private val withoutBlankTemplate: Boolean get() = argBoolean(WITH_BLANK_TEMPLATE_KEY)
private lateinit var templatesAdapter: TemplateSelectAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.templateViewPager.adapter = Adapter(ids, this)
TabLayoutMediator(binding.tabs, binding.templateViewPager) { _, _ -> }.attach()
vm.navigation.observe(viewLifecycleOwner, navObserver)
setupViewPagerAndTabs()
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { setupTemplateHeaderMessage() }
launch { setupCancelClicks() }
launch { setupUseTemplateClicks() }
launch { setupDismissStatusObserver() }
setupClickEventHandlers()
}
}
}
private fun setupViewPagerAndTabs() {
templatesAdapter = TemplateSelectAdapter(mutableListOf(), this)
binding.templateViewPager.adapter = templatesAdapter
TabLayoutMediator(binding.tabs, binding.templateViewPager) { _, _ -> }.attach()
}
private suspend fun setupClickEventHandlers() {
setupUseTemplateClicks()
setupCancelClicks()
}
override fun onStart() {
jobs += lifecycleScope.subscribe(vm.viewState) { render(it) }
jobs += lifecycleScope.subscribe(vm.isDismissed) { if (it) exit() }
super.onStart()
vm.onStart(type = type, withoutBlankTemplate = withoutBlankTemplate)
}
private fun render(viewState: TemplateSelectViewModel.ViewState) {
when (viewState) {
TemplateSelectViewModel.ViewState.ErrorGettingType -> TODO()
TemplateSelectViewModel.ViewState.Init -> {
binding.tvTemplateCountOrTutorial.text = null
binding.btnCancel.isEnabled = true
binding.btnUseTemplate.isEnabled = false
}
is TemplateSelectViewModel.ViewState.Success -> {
binding.tvTemplateCountOrTutorial.text = getString(
R.string.this_type_has_templates,
viewState.objectTypeName,
viewState.templates.size
)
binding.btnUseTemplate.isEnabled = true
templatesAdapter.update(viewState.templates)
}
}
}
private suspend fun setupUseTemplateClicks() {
binding.btnUseTemplate.clicks().collect {
vm.onUseTemplate(
template = ids[binding.templateViewPager.currentItem],
vm.onUseTemplateButtonPressed(
currentItem = binding.templateViewPager.currentItem,
ctx = ctx
)
}
@ -65,22 +99,10 @@ class TemplateSelectFragment :
binding.btnCancel.clicks().collect { exit() }
}
private suspend fun setupDismissStatusObserver() {
vm.isDismissed.collect { isDismissed -> if (isDismissed) exit() }
}
private fun exit() {
findNavController().popBackStack()
}
private suspend fun setupTemplateHeaderMessage() {
binding.tvTemplateCountOrTutorial.text = getString(
R.string.this_type_has_templates, ids.size
)
delay(USE_SWIPE_TO_CHOOSE_MSG_DELAY)
binding.tvTemplateCountOrTutorial.setText(R.string.swipe_to_choose)
}
override fun injectDependencies() {
componentManager().templateSelectComponent.get().inject(this)
}
@ -97,19 +119,8 @@ class TemplateSelectFragment :
)
companion object {
const val TEMPLATE_IDS_KEY = "arg.template.ids"
const val WITH_BLANK_TEMPLATE_KEY = "arg.template.with_empty_template"
const val OBJECT_TYPE_KEY = "arg.template.object_type"
const val CTX_KEY = "arg.template.ctx"
private const val USE_SWIPE_TO_CHOOSE_MSG_DELAY = 2000L
}
internal class Adapter(
private val ids: List<Id>, fragment: Fragment
) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = ids.size
override fun createFragment(position: Int): Fragment {
return TemplateFragment.new(ids[position])
}
}
}

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--typography, buttons 05.04-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -22,8 +21,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="11dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="11dp"
android:gravity="center"
android:maxLines="1"
android:singleLine="true"
@ -51,7 +51,7 @@
android:layout_marginStart="20dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:text="@string/without_template" />
android:text="@string/cancel" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimaryLarge
android:id="@+id/btnUseTemplate"

View file

@ -154,6 +154,13 @@ sealed class ObjectWrapper {
val iconEmoji: String? by default
val isDeleted: Boolean? by default
val recommendedRelations: List<Id> get() = getValues(Relations.RECOMMENDED_RELATIONS)
val recommendedLayout: ObjectType.Layout?
get() = when (val value = map[Relations.RECOMMENDED_LAYOUT]) {
is Double -> ObjectType.Layout.values().singleOrNull { layout ->
layout.code == value.toInt()
}
else -> null
}
}
data class Relation(override val map: Struct) : ObjectWrapper() {

View file

@ -64,6 +64,9 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
focus(item.isFocused)
}
content.pauseTextWatchers {
if (item.hint != null) {
content.hint = item.hint
}
content.setText(item.text, TextView.BufferType.EDITABLE)
}
cover?.setOnClickListener { onCoverClicked() }

View file

@ -16,9 +16,11 @@ class TypeHasTemplateToolbarWidget @JvmOverloads constructor(
LayoutInflater.from(context), this, true
)
var count: Int = 0
set(value) {
field = value
binding.tvTitle.text = resources.getString(R.string.this_type_has_templates, value)
}
fun setText(count: Int, typeName: String) {
binding.tvTitle.text = context.getString(
R.string.this_type_has_templates,
typeName,
count
)
}
}

View file

@ -1,46 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--typography, buttons 05.04-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<View
android:id="@+id/dragger"
android:layout_width="@dimen/default_dragger_width"
android:layout_height="@dimen/default_dragger_height"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:background="@drawable/dragger" />
android:background="@drawable/dragger"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
style="@style/TextView.UXStyle.Captions.1.Medium"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="24dp"
tools:text="This type has 2 templates" />
android:layout_marginEnd="20dp"
app:layout_constraintEnd_toStartOf="@+id/btnShow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dragger"
tools:text="This type has 2 templates " />
<TextView
android:id="@+id/tvSubtitle"
style="@style/TextView.UXStyle.Body"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="45dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="24dp"
android:text="@string/swipe_down_to_skip"
android:textColor="@color/text_secondary" />
android:textColor="@color/text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnShow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
<com.anytypeio.anytype.core_ui.views.ButtonSecondaryMedium
android:id="@+id/btnShow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginTop="24dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="24dp"
android:text="@string/show" />
android:text="@string/show"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -553,7 +553,7 @@
<string name="swipe_down_to_skip">Swipe down to skip</string>
<string name="show">Show</string>
<string name="this_type_has_templates">This type has %1$d templates</string>
<string name="this_type_has_templates">Type \"%1$s\" has %2$d templates</string>
<string name="item_relation_create_from_scratch_title">Type</string>
<string name="btn_restore">Restore</string>

View file

@ -31,16 +31,12 @@ class CreateObject(
null
}
val template = objectTemplates?.singleOrNull()?.id
val internalFlags = buildList {
if (objectTemplates != null && objectTemplates.size > 1) {
if (!objectTemplates.isNullOrEmpty()) {
add(InternalFlags.ShouldSelectType)
add(InternalFlags.ShouldSelectTemplate)
} else {
if (template == null) {
add(InternalFlags.ShouldSelectType)
}
add(InternalFlags.ShouldSelectType)
}
add(InternalFlags.ShouldEmptyDelete)
}
@ -50,7 +46,7 @@ class CreateObject(
}
val command = Command.CreateObject(
template = template,
template = null,
prefilled = prefilled,
internalFlags = internalFlags
)
@ -60,7 +56,7 @@ class CreateObject(
return Result(
objectId = result.id,
event = result.event,
appliedTemplate = template,
appliedTemplate = null,
type = type
)
}

View file

@ -51,7 +51,8 @@ class ObjectTypesSubscriptionManager (
Relations.DESCRIPTION,
Relations.ICON_EMOJI,
Relations.SOURCE_OBJECT,
Relations.IS_READ_ONLY
Relations.IS_READ_ONLY,
Relations.RECOMMENDED_LAYOUT
),
ignoreWorkspace = true
)

View file

@ -125,8 +125,10 @@ class CreateObjectTest {
verifyBlocking(getTemplates, times(1)) { run(GetTemplates.Params(defaultType)) }
val commands = Command.CreateObject(
prefilled = buildMap { put(Relations.TYPE, defaultType) },
template = templateBook,
template = null,
internalFlags = listOf(
InternalFlags.ShouldSelectType,
InternalFlags.ShouldSelectTemplate,
InternalFlags.ShouldEmptyDelete
)
)
@ -181,8 +183,10 @@ class CreateObjectTest {
verifyBlocking(getTemplates, times(1)) { run(GetTemplates.Params(type)) }
val commands = Command.CreateObject(
prefilled = buildMap { put(Relations.TYPE, type) },
template = template,
template = null,
internalFlags = listOf(
InternalFlags.ShouldSelectType,
InternalFlags.ShouldSelectTemplate,
InternalFlags.ShouldEmptyDelete
)
)

View file

@ -311,7 +311,8 @@ class EditorViewModel(
when (state) {
is SelectTemplateState.Available -> {
SelectTemplateViewState.Active(
count = state.templates.size
count = state.templates.size,
typeName = state.typeName
)
}
else -> SelectTemplateViewState.Idle
@ -415,7 +416,6 @@ class EditorViewModel(
EventWrapper(
AppNavigation.Command.OpenTemplates(
type = state.type,
templates = state.templates,
ctx = context
)
)
@ -6188,12 +6188,18 @@ class EditorViewModel(
private fun proceedWithTemplateSelection(typeId: Id) {
viewModelScope.launch {
onEvent(
SelectTemplateEvent.OnStart(
ctx = context,
type = typeId
val objType = storeOfObjectTypes.get(typeId)
if (objType != null) {
onEvent(
SelectTemplateEvent.OnStart(
ctx = context,
type = typeId,
typeName = objType.name.orEmpty()
)
)
)
} else {
Timber.e("Error while getting object type from storeOfObjectTypes by id: $typeId")
}
}
}

View file

@ -639,6 +639,7 @@ sealed class BlockView : ViewType {
abstract var coverGradient: String?
override val color: ThemeColor = ThemeColor.DEFAULT
abstract override val background: ThemeColor
abstract val hint: String?
val hasCover get() = coverColor != null || coverImage != null || coverGradient != null
@ -660,7 +661,8 @@ sealed class BlockView : ViewType {
override val image: String? = null,
override val mode: Mode = Mode.EDIT,
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList()
override val searchFields: List<Searchable.Field> = emptyList(),
override val hint: String? = null
) : Title(), Searchable {
override fun getViewType() = HOLDER_TITLE
}
@ -683,7 +685,8 @@ sealed class BlockView : ViewType {
override val image: String? = null,
override val mode: Mode = Mode.EDIT,
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList()
override val searchFields: List<Searchable.Field> = emptyList(),
override val hint: String? = null
) : Title(), Searchable {
override fun getViewType() = HOLDER_PROFILE_TITLE
}
@ -707,6 +710,7 @@ sealed class BlockView : ViewType {
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
var isChecked: Boolean = false,
override val hint: String? = null
) : Title(), Searchable {
override fun getViewType() = HOLDER_TODO_TITLE
}
@ -728,7 +732,8 @@ sealed class BlockView : ViewType {
override val background: ThemeColor = ThemeColor.DEFAULT,
override val color: ThemeColor = ThemeColor.DEFAULT,
override val mode: Mode = Mode.READ,
override var cursor: Int? = null
override var cursor: Int? = null,
override val hint: String? = null
) : Title() {
override fun getViewType() = HOLDER_ARCHIVE_TITLE
}

View file

@ -27,21 +27,11 @@ class DefaultEditorTemplateDelegate(
try {
val templates = getTemplates.run(GetTemplates.Params(event.type))
if (templates.isNotEmpty()) {
if (templates.size == 1) {
// No need to choose template if there is only one available template.
applyTemplate.run(
ApplyTemplate.Params(
ctx = event.ctx,
template = templates.first().id
)
)
SelectTemplateState.Idle
} else {
SelectTemplateState.Available(
templates = templates.map { it.id },
type = event.type
)
}
SelectTemplateState.Available(
templates = templates.map { it.id },
type = event.type,
typeName = event.typeName
)
} else {
SelectTemplateState.Idle
}
@ -77,7 +67,8 @@ sealed class SelectTemplateState {
*/
data class Available(
val type: Id,
val templates: List<Id>
val templates: List<Id>,
val typeName: String
) : SelectTemplateState()
/**
@ -94,7 +85,7 @@ sealed class SelectTemplateState {
}
sealed class SelectTemplateEvent {
data class OnStart(val ctx: Id, val type: Id) : SelectTemplateEvent()
data class OnStart(val ctx: Id, val type: Id, val typeName: String) : SelectTemplateEvent()
object OnSkipped : SelectTemplateEvent()
object OnAccepted : SelectTemplateEvent()
}

View file

@ -2,5 +2,5 @@ package com.anytypeio.anytype.presentation.editor.template
sealed class SelectTemplateViewState {
object Idle : SelectTemplateViewState()
data class Active(val count: Int) : SelectTemplateViewState()
data class Active(val count: Int, val typeName: String) : SelectTemplateViewState()
}

View file

@ -52,8 +52,7 @@ interface AppNavigation {
fun openTemplates(
ctx: Id,
type: String,
templates: List<Id>
type: String
)
fun openLibrary()
@ -117,8 +116,7 @@ interface AppNavigation {
data class OpenTemplates(
val ctx: Id,
val type: String,
val templates: List<Id>
val type: String
) : Command()
object OpenLibrary: Command()

View file

@ -0,0 +1,102 @@
package com.anytypeio.anytype.presentation.templates
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.asMap
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.render.BlockViewRenderer
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.templates.TemplateConstants.BLANK_ROOT_ID
import com.anytypeio.anytype.presentation.templates.TemplateConstants.BLANK_TITLE
import com.anytypeio.anytype.presentation.templates.TemplateConstants.HEADER_ID
import com.anytypeio.anytype.presentation.templates.TemplateConstants.TITLE_ID
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class TemplateBlankViewModel(
private val renderer: DefaultBlockViewRenderer,
) : BaseViewModel(), BlockViewRenderer by renderer {
val state = MutableStateFlow<List<BlockView>>(emptyList())
fun onStart(typeId: Id, typeName: String, layout: Int) {
Timber.d("onStart, typeId: $typeId, typeName: $typeName, layout: $layout")
val blockTitle = Block(
id = TITLE_ID,
content = Block.Content.Text(
text = "",
style = Block.Content.Text.Style.TITLE,
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty(),
)
val featuredRelationsBlock = Block(
id = Relations.FEATURED_RELATIONS,
content = Block.Content.FeaturedRelations,
children = emptyList(),
fields = Block.Fields.empty(),
)
val headerBlock = Block(
id = HEADER_ID,
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
children = listOf(blockTitle.id, featuredRelationsBlock.id),
fields = Block.Fields.empty(),
)
val rootBlock = Block(
id = BLANK_ROOT_ID,
content = Block.Content.Smart,
children = listOf(headerBlock.id),
fields = Block.Fields.empty(),
)
val featuredRelations = listOf(Relations.TYPE)
val page = listOf(rootBlock, headerBlock, blockTitle, featuredRelationsBlock)
val objectDetails = Block.Fields(
mapOf(
Relations.LAYOUT to layout,
Relations.TYPE to typeId,
Relations.FEATURED_RELATIONS to featuredRelations
)
)
val typeDetails = Block.Fields(
mapOf(
Relations.ID to typeId,
Relations.NAME to typeName
)
)
val customDetails =
Block.Details(mapOf(BLANK_ROOT_ID to objectDetails, typeId to typeDetails))
viewModelScope.launch {
val blockViews = page.asMap().render(
mode = Editor.Mode.Read,
root = page.first(),
focus = com.anytypeio.anytype.domain.editor.Editor.Focus.empty(),
anchor = page.first().id,
indent = EditorViewModel.INITIAL_INDENT,
details = customDetails,
relationLinks = emptyList(),
restrictions = emptyList(),
selection = emptySet()
)
state.value = blockViews.map {
when (it) {
is BlockView.Title.Basic -> it.copy(hint = BLANK_TITLE)
is BlockView.Title.Profile -> it.copy(hint = BLANK_TITLE)
is BlockView.Title.Todo -> it.copy(hint = BLANK_TITLE)
else -> it
}
}
}
}
}

View file

@ -0,0 +1,18 @@
package com.anytypeio.anytype.presentation.templates
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import javax.inject.Inject
class TemplateBlankViewModelFactory @Inject constructor(
private val renderer: DefaultBlockViewRenderer,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TemplateBlankViewModel(
renderer = renderer
) as T
}
}

View file

@ -5,47 +5,148 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.navigation.SupportNavigation
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
class TemplateSelectViewModel(
private val storeOfObjectTypes: StoreOfObjectTypes,
private val getTemplates: GetTemplates,
private val applyTemplate: ApplyTemplate
) : BaseViewModel(), SupportNavigation<EventWrapper<AppNavigation.Command>> {
val isDismissed = MutableSharedFlow<Boolean>(replay = 0)
val isDismissed = MutableStateFlow(false)
override val navigation: MutableLiveData<EventWrapper<AppNavigation.Command>> = MutableLiveData()
private val _viewState = MutableStateFlow<ViewState>(ViewState.Init)
val viewState: StateFlow<ViewState> = _viewState
fun onUseTemplate(ctx: Id, template: Id) {
override val navigation: MutableLiveData<EventWrapper<AppNavigation.Command>> =
MutableLiveData()
fun onStart(type: Id, withoutBlankTemplate: Boolean) {
viewModelScope.launch {
val result = applyTemplate.execute(
ApplyTemplate.Params(
ctx = ctx,
template = template
val objType = storeOfObjectTypes.get(type)
if (objType != null) {
Timber.d("onStart, Object type $objType")
proceedWithGettingTemplates(objType, withoutBlankTemplate)
} else {
Timber.e("onStart, Object type $type not found")
}
}
}
private fun proceedWithGettingTemplates(
objType: ObjectWrapper.Type, withoutBlankTemplate: Boolean
) {
val params = GetTemplates.Params(objType.id)
viewModelScope.launch {
getTemplates.async(params)
.fold(
onSuccess = { buildTemplateViews(objType, it, withoutBlankTemplate) },
onFailure = { Timber.e(it, "Error while getting templates") })
}
}
private suspend fun buildTemplateViews(
objType: ObjectWrapper.Type,
templates: List<ObjectWrapper.Basic>,
withoutBlankTemplate: Boolean
) {
val templateViews = buildList {
if (!withoutBlankTemplate) add(
TemplateView.Blank(
typeId = objType.id,
typeName = objType.name.orEmpty(),
layout = objType.recommendedLayout?.code ?: 0
)
)
if (result.isFailure) {
sendToast("Something went wrong. Please, try again later.")
addAll(templates.map { TemplateView.Template(it.id) })
}
_viewState.emit(
ViewState.Success(
objectTypeName = objType.name.orEmpty(),
templates = templateViews,
)
)
}
fun onUseTemplateButtonPressed(ctx: Id, currentItem: Int) {
when (val state = _viewState.value) {
is ViewState.Success -> {
when (val template = state.templates[currentItem]) {
is TemplateView.Blank -> {
isDismissed.value = true
}
is TemplateView.Template -> {
proceedWithApplyingTemplate(ctx, template)
}
}
}
isDismissed.emit(true)
else -> {
Timber.e("onUseTemplate: unexpected state $state")
isDismissed.value = true
}
}
}
private fun proceedWithApplyingTemplate(ctx: Id, template: TemplateView.Template) {
val params = ApplyTemplate.Params(ctx = ctx, template = template.id)
viewModelScope.launch {
applyTemplate.async(params).fold(
onSuccess = {
isDismissed.value = true
Timber.d("Template ${template.id} applied successfully")
},
onFailure = {
isDismissed.value = true
Timber.e(it, "Error while applying template")
sendToast("Something went wrong. Please, try again later.")
}
)
}
}
class Factory @Inject constructor(
private val applyTemplate: ApplyTemplate
private val applyTemplate: ApplyTemplate,
private val getTemplates: GetTemplates,
private val storeOfObjectTypes: StoreOfObjectTypes
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TemplateSelectViewModel(
applyTemplate = applyTemplate
applyTemplate = applyTemplate,
getTemplates = getTemplates,
storeOfObjectTypes = storeOfObjectTypes
) as T
}
}
sealed class ViewState {
data class Success(
val objectTypeName: String, val templates: List<TemplateView>
) : ViewState()
object Init : ViewState()
object ErrorGettingType : ViewState()
}
sealed class TemplateView {
data class Blank(
val typeId: Id, val typeName: String, val layout: Int
) : TemplateView()
data class Template(val id: Id) : TemplateView()
}
}

View file

@ -0,0 +1,8 @@
package com.anytypeio.anytype.presentation.templates
object TemplateConstants {
const val BLANK_TITLE = "Blank template"
const val BLANK_ROOT_ID = "blank_template_root_id"
const val HEADER_ID = "header"
const val TITLE_ID = "blockTitle"
}