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

Feature/emoji pack (#542)

This commit is contained in:
Evgenii Kozlov 2020-06-24 13:13:45 +02:00 committed by GitHub
parent 7c32a77909
commit 45a6430c70
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 19861 additions and 556 deletions

File diff suppressed because it is too large Load diff

View file

@ -122,10 +122,17 @@ class ComponentManager(private val main: MainComponent) {
.build()
}
val pageIconPickerSubComponent = Component {
val documentIconActionMenuComponent = Component {
main
.pageIconPickerBuilder()
.documentIconPickerModule(DocumentIconPickerModule())
.documentActionMenuComponentBuilder()
.documentIconActionMenuModule(DocumentIconActionMenuModule())
.build()
}
val documentEmojiIconPickerComponent = Component {
main
.documentEmojiIconPickerComponentBuilder()
.documentIconActionMenuModule(DocumentEmojiIconPickerModule())
.build()
}

View file

@ -0,0 +1,43 @@
package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
import com.agileburo.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(modules = [DocumentEmojiIconPickerModule::class])
@PerScreen
interface DocumentEmojiIconPickerSubComponent {
@Subcomponent.Builder
interface Builder {
fun documentIconActionMenuModule(module: DocumentEmojiIconPickerModule): Builder
fun build(): DocumentEmojiIconPickerSubComponent
}
fun inject(fragment: DocumentEmojiIconPickerFragment)
}
@Module
class DocumentEmojiIconPickerModule {
@Provides
@PerScreen
fun provideDocumentEmojiIconPickerViewModel(
setEmojiIcon: SetDocumentEmojiIcon
): DocumentEmojiIconPickerViewModelFactory = DocumentEmojiIconPickerViewModelFactory(
setEmojiIcon = setEmojiIcon
)
@Provides
@PerScreen
fun provideSetDocumentEmojiIconUseCase(
repo: BlockRepository
): SetDocumentEmojiIcon = SetDocumentEmojiIcon(
repo = repo
)
}

View file

@ -4,36 +4,34 @@ import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.domain.icon.SetDocumentImageIcon
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModelFactory
import com.agileburo.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
import com.agileburo.anytype.ui.page.modals.actions.DocumentIconActionMenu
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModelFactory
import com.agileburo.anytype.ui.page.modals.actions.DocumentIconActionMenuFragment
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(modules = [DocumentIconPickerModule::class])
@Subcomponent(modules = [DocumentIconActionMenuModule::class])
@PerScreen
interface DocumentIconPickerSubComponent {
interface DocumentActionMenuSubComponent {
@Subcomponent.Builder
interface Builder {
fun documentIconPickerModule(module: DocumentIconPickerModule): Builder
fun build(): DocumentIconPickerSubComponent
fun documentIconActionMenuModule(module: DocumentIconActionMenuModule): Builder
fun build(): DocumentActionMenuSubComponent
}
fun inject(fragment: DocumentEmojiIconPickerFragment)
fun inject(fragment: DocumentIconActionMenu)
fun inject(fragment: DocumentIconActionMenuFragment)
}
@Module
class DocumentIconPickerModule {
class DocumentIconActionMenuModule {
@Provides
@PerScreen
fun provideDocumentIconPickerViewModelFactory(
fun provideDocumentIconActionMenuViewModelFactory(
setEmojiIcon: SetDocumentEmojiIcon,
setImageIcon: SetDocumentImageIcon
): DocumentIconPickerViewModelFactory = DocumentIconPickerViewModelFactory(
): DocumentIconActionMenuViewModelFactory = DocumentIconActionMenuViewModelFactory(
setEmojiIcon = setEmojiIcon,
setImageIcon = setImageIcon
)

View file

@ -33,7 +33,8 @@ interface MainComponent {
fun detailsReorderBuilder(): DetailsReorderSubComponent.Builder
fun pageComponentBuilder(): PageSubComponent.Builder
fun linkAddComponentBuilder(): LinkSubComponent.Builder
fun pageIconPickerBuilder(): DocumentIconPickerSubComponent.Builder
fun documentActionMenuComponentBuilder(): DocumentActionMenuSubComponent.Builder
fun documentEmojiIconPickerComponentBuilder(): DocumentEmojiIconPickerSubComponent.Builder
fun createBookmarkBuilder(): CreateBookmarkSubComponent.Builder
fun debugSettingsBuilder() : DebugSettingsSubComponent.Builder
}

View file

@ -8,8 +8,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.agileburo.anytype.R
import com.agileburo.anytype.core_ui.tools.SupportDragAndDropBehavior
import com.agileburo.anytype.core_utils.ext.shift
import com.agileburo.anytype.emojifier.Emojifier
import com.agileburo.anytype.presentation.desktop.DashboardView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.item_desktop_page.view.*
class DashboardAdapter(
@ -60,10 +62,13 @@ class DashboardAdapter(
class DocumentHolder(itemView: View) : ViewHolder(itemView) {
private val title = itemView.title
private val emoji = itemView.emoji
private val emoji = itemView.emojiIcon
private val image = itemView.image
fun bind(doc: DashboardView.Document, onClick: (DashboardView.Document) -> Unit) {
fun bind(
doc: DashboardView.Document,
onClick: (DashboardView.Document) -> Unit
) {
itemView.setOnClickListener { onClick(doc) }
if (doc.title.isNullOrEmpty())
@ -71,7 +76,13 @@ class DashboardAdapter(
else
title.text = doc.title
emoji.text = doc.emoji ?: EMPTY_EMOJI
doc.emoji?.let { unicode ->
Glide
.with(emoji)
.load(Emojifier.uri(unicode))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emoji)
}
doc.image?.let { url ->
Glide
@ -86,7 +97,6 @@ class DashboardAdapter(
}
fun update(views: List<DashboardView>) {
val callback = DesktopDiffUtil(
old = data,
new = views

View file

@ -64,7 +64,7 @@ import com.agileburo.anytype.ui.base.NavigationFragment
import com.agileburo.anytype.ui.menu.AnytypeContextMenu
import com.agileburo.anytype.ui.page.modals.*
import com.agileburo.anytype.ui.page.modals.actions.BlockActionToolbarFactory
import com.agileburo.anytype.ui.page.modals.actions.DocumentIconActionMenu
import com.agileburo.anytype.ui.page.modals.actions.DocumentIconActionMenuFragment
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.hbisoft.pickit.PickiT
@ -514,7 +514,7 @@ open class PageFragment :
recycler.smoothScrollToPosition(0)
val shared =
recycler.getChildAt(0).findViewById<FrameLayout>(R.id.documentIconContainer)
val fr = DocumentIconActionMenu.new(
val fr = DocumentIconActionMenuFragment.new(
y = shared.y + dimen(R.dimen.dp_48),
emoji = command.emoji,
target = command.target,
@ -531,7 +531,7 @@ open class PageFragment :
.commit()
}
is Command.OpenDocumentEmojiIconPicker -> {
DocumentEmojiIconPickerFragment.newInstance(
DocumentEmojiIconPickerFragment.new(
context = requireArguments().getString(ID_KEY, ID_EMPTY_VALUE),
target = command.target
).show(childFragmentManager, null)

View file

@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.agileburo.anytype.R
import com.agileburo.anytype.core_utils.ext.toast
@ -17,13 +17,14 @@ import com.agileburo.anytype.core_utils.ui.BaseBottomSheetFragment
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerAdapter
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel.Contract
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel.ViewState
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModelFactory
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModel
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModel.ViewState
import com.agileburo.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.android.synthetic.main.fragment_page_icon_picker.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
@ -38,27 +39,24 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
.getString(ARG_CONTEXT_ID_KEY)
?: throw IllegalStateException(MISSING_CONTEXT_ERROR)
@Inject
lateinit var factory: DocumentIconPickerViewModelFactory
private val vm by lazy {
ViewModelProviders
.of(this, factory)
.get(DocumentIconPickerViewModel::class.java)
.get(DocumentEmojiIconPickerViewModel::class.java)
}
private val pageIconPickerAdapter by lazy {
@Inject
lateinit var factory: DocumentEmojiIconPickerViewModelFactory
private val emojiPickerAdapter by lazy {
DocumentEmojiIconPickerAdapter(
views = emptyList(),
onFilterQueryChanged = { vm.onEvent(Contract.Event.OnFilterQueryChanged(it)) },
onEmojiClicked = { unicode, alias ->
vm.onEvent(
Contract.Event.OnEmojiClicked(
unicode = unicode,
alias = alias,
target = target,
context = context
)
onFilterQueryChanged = { toast("not implemented yet") },
onEmojiClicked = { unicode ->
vm.onEmojiClicked(
unicode = unicode,
target = target,
context = context
)
}
)
@ -83,14 +81,17 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecycler()
}
private fun setupRecycler() {
recyler.apply {
setItemViewCacheSize(EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE)
setHasFixedSize(true)
layoutManager = GridLayoutManager(context, PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) =
when (val type = pageIconPickerAdapter.getItemViewType(position)) {
when (val type = emojiPickerAdapter.getItemViewType(position)) {
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_ITEM -> 1
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_CATEGORY_HEADER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT
DocumentEmojiIconPickerViewHolder.HOLDER_EMOJI_FILTER -> PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT
@ -98,36 +99,31 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
}
}
}
adapter = pageIconPickerAdapter.apply {
setHasStableIds(true)
}
adapter = emojiPickerAdapter
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
vm.state.observe(viewLifecycleOwner, Observer { render(it) })
vm.state().onEach { state ->
when (state) {
is ViewState.Success -> emojiPickerAdapter.update(state.views)
is ViewState.Exit -> dismiss()
}
}.launchIn(lifecycleScope)
}
override fun onDestroyView() {
dialog?.setOnShowListener { null }
dialog?.setOnShowListener(null)
super.onDestroyView()
}
fun render(state: ViewState) {
when (state) {
is ViewState.Success -> pageIconPickerAdapter.update(state.views)
is ViewState.Exit -> dismiss()
is ViewState.Error -> toast(state.message)
}
}
override fun injectDependencies() {
componentManager().pageIconPickerSubComponent.get().inject(this)
componentManager().documentEmojiIconPickerComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().pageIconPickerSubComponent.release()
componentManager().documentEmojiIconPickerComponent.release()
}
private fun setModalToFullScreenState(dialog: BottomSheetDialog) =
@ -147,15 +143,15 @@ class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() {
companion object {
fun newInstance(
context: String,
target: String
) = DocumentEmojiIconPickerFragment().apply {
arguments = bundleOf(ARG_CONTEXT_ID_KEY to context, ARG_TARGET_ID_KEY to target)
fun new(context: String, target: String) = DocumentEmojiIconPickerFragment().apply {
arguments = bundleOf(
ARG_CONTEXT_ID_KEY to context,
ARG_TARGET_ID_KEY to target
)
}
private const val PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT = 8
private const val EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE = 100
private const val PAGE_ICON_PICKER_DEFAULT_SPAN_COUNT = 6
private const val EMOJI_RECYCLER_ITEM_VIEW_CACHE_SIZE = 2000
private const val ARG_CONTEXT_ID_KEY = "arg.picker.context.id"
private const val ARG_TARGET_ID_KEY = "arg.picker.target.id"
private const val MISSING_TARGET_ERROR = "Missing target id"

View file

@ -17,21 +17,25 @@ import com.agileburo.anytype.core_utils.ext.parsePath
import com.agileburo.anytype.core_utils.ext.toast
import com.agileburo.anytype.core_utils.ui.BaseFragment
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.emojifier.Emojifier
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter.Companion.OPTION_CHOOSE_EMOJI
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter.Companion.OPTION_CHOOSE_RANDOM_EMOJI
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter.Companion.OPTION_CHOOSE_UPLOAD_PHOTO
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter.Companion.OPTION_REMOVE
import com.agileburo.anytype.library_page_icon_picker_widget.ui.ActionMenuDivider
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModelFactory
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.Contract
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.ViewState
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModelFactory
import com.agileburo.anytype.ui.page.modals.DocumentEmojiIconPickerFragment
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.action_toolbar_page_icon.*
import javax.inject.Inject
class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
Observer<DocumentIconPickerViewModel.ViewState> {
class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page_icon),
Observer<ViewState> {
private val target: String
get() = requireArguments()
@ -39,12 +43,12 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
?: throw IllegalStateException(MISSING_TARGET_ERROR)
@Inject
lateinit var factory: DocumentIconPickerViewModelFactory
lateinit var factory: DocumentIconActionMenuViewModelFactory
private val vm by lazy {
ViewModelProviders
.of(this, factory)
.get(DocumentIconPickerViewModel::class.java)
.get(DocumentIconActionMenuViewModel::class.java)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -62,7 +66,13 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
}
private fun setIcon() {
arguments?.getString(EMOJI_KEY)?.let { emoji -> emojiIcon.text = emoji }
arguments?.getString(EMOJI_KEY)?.let { unicode ->
Glide
.with(emojiIconImage)
.load(Emojifier.uri(unicode))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emojiIconImage)
}
arguments?.getString(IMAGE_KEY)?.let { url ->
Glide
.with(icon)
@ -120,20 +130,20 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
OPTION_CHOOSE_EMOJI -> {
parentFragment?.childFragmentManager?.let { manager ->
manager.popBackStack()
DocumentEmojiIconPickerFragment.newInstance(
DocumentEmojiIconPickerFragment.new(
context = target,
target = target
).show(manager, null)
}
}
OPTION_REMOVE -> vm.onEvent(
DocumentIconPickerViewModel.Contract.Event.OnRemoveEmojiSelected(
Contract.Event.OnRemoveEmojiSelected(
context = target,
target = target
)
)
OPTION_CHOOSE_RANDOM_EMOJI -> vm.onEvent(
DocumentIconPickerViewModel.Contract.Event.OnSetRandomEmojiClicked(
Contract.Event.OnSetRandomEmojiClicked(
context = target,
target = target
)
@ -147,10 +157,11 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
}
}
override fun onChanged(state: DocumentIconPickerViewModel.ViewState) {
override fun onChanged(state: ViewState) {
when (state) {
is DocumentIconPickerViewModel.ViewState.Exit -> exit()
is DocumentIconPickerViewModel.ViewState.Error -> toast(state.message)
is ViewState.Exit -> exit()
is ViewState.Error -> toast(state.message)
is ViewState.Loading -> toast(getString(R.string.loading))
}
}
@ -178,9 +189,11 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
if (resultCode == RESULT_OK && requestCode == SELECT_IMAGE_CODE) {
data?.data?.let { uri ->
val path = uri.parsePath(requireContext())
vm.onImagePickedFromGallery(
context = target,
path = path
vm.onEvent(
Contract.Event.OnImagePickedFromGallery(
context = target,
path = path
)
)
}
}
@ -200,11 +213,11 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
}
override fun injectDependencies() {
componentManager().pageIconPickerSubComponent.get().inject(this)
componentManager().documentIconActionMenuComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().pageIconPickerSubComponent.release()
componentManager().documentIconActionMenuComponent.release()
}
companion object {
@ -213,7 +226,7 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
emoji: String?,
image: String?,
target: String
): DocumentIconActionMenu = DocumentIconActionMenu().apply {
): DocumentIconActionMenuFragment = DocumentIconActionMenuFragment().apply {
arguments = bundleOf(
Y_KEY to y,
EMOJI_KEY to emoji,
@ -223,7 +236,7 @@ class DocumentIconActionMenu : BaseFragment(R.layout.action_toolbar_page_icon),
}
private const val SELECT_IMAGE_CODE = 1
const val REQUEST_PERMISSION_CODE = 2
private const val REQUEST_PERMISSION_CODE = 2
private const val Y_KEY = "y"
private const val EMOJI_KEY = "emoji"
private const val IMAGE_KEY = "image_key"

View file

@ -27,10 +27,17 @@
android:textSize="28sp"
tools:text="🚀" />
<ImageView
android:id="@+id/emojiIconImage"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center" />
<ImageView
android:id="@+id/imageIcon"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<androidx.cardview.widget.CardView

View file

@ -28,6 +28,12 @@
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
android:id="@+id/emojiIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center" />
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"

View file

@ -44,6 +44,7 @@ dependencies {
def unitTestDependencies = rootProject.ext.unitTesting
implementation project(':core-utils')
implementation project(':library-emojifier')
implementation applicationDependencies.appcompat
implementation applicationDependencies.kotlin

View file

@ -96,8 +96,6 @@ class GoalAdapter(
fun bind(item: GoalView.Title) {
title.setText(item.title)
logo.text = ""
}
}

View file

@ -555,6 +555,7 @@ sealed class BlockView : ViewType, Parcelable {
override val isSelected: Boolean = false,
val text: String? = null,
val emoji: String?,
val image: String?,
val isEmpty: Boolean = false,
val isArchived: Boolean = false
) : BlockView(), Indentable, Selectable {

View file

@ -39,8 +39,10 @@ import com.agileburo.anytype.core_ui.widgets.text.EditorLongClickListener
import com.agileburo.anytype.core_ui.widgets.text.TextInputWidget
import com.agileburo.anytype.core_utils.const.MimeTypes
import com.agileburo.anytype.core_utils.ext.*
import com.agileburo.anytype.emojifier.Emojifier
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
@ -203,10 +205,17 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
.into(image)
} ?: apply { image.setImageDrawable(null) }
if (item.emoji != null) {
Glide
.with(emoji)
.load(Emojifier.uri(item.emoji))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emoji)
}
if (item.mode == BlockView.Mode.READ) {
enableReadOnlyMode()
content.setText(item.text, BufferType.EDITABLE)
emoji.text = item.emoji ?: EMPTY_EMOJI
} else {
enableEditMode()
if (item.isFocused) setCursor(item)
@ -218,7 +227,6 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
onFocusChanged(item.id, hasFocus)
if (hasFocus) showKeyboard()
}
emoji.text = item.emoji ?: EMPTY_EMOJI
icon.setOnClickListener { onPageIconClicked() }
}
}
@ -1478,7 +1486,8 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val untitled = itemView.resources.getString(R.string.untitled)
private val icon = itemView.pageIcon
private val emoji = itemView.emoji
private val emoji = itemView.linkEmoji
private val image = itemView.linkImage
private val title = itemView.pageTitle
private val guideline = itemView.pageGuideline
@ -1493,9 +1502,31 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
title.text = if (item.text.isNullOrEmpty()) untitled else item.text
when {
item.emoji != null -> emoji.text = item.emoji
item.isEmpty -> icon.setImageResource(R.drawable.ic_block_empty_page)
else -> icon.setImageResource(R.drawable.ic_block_page_without_emoji)
item.emoji != null -> {
image.setImageDrawable(null)
Glide
.with(emoji)
.load(Emojifier.uri(item.emoji))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(emoji)
}
item.image != null -> {
image.visible()
Glide
.with(image)
.load(item.image)
.centerInside()
.circleCrop()
.into(image)
}
item.isEmpty -> {
icon.setImageResource(R.drawable.ic_block_empty_page)
image.setImageDrawable(null)
}
else -> {
icon.setImageResource(R.drawable.ic_block_page_without_emoji)
image.setImageDrawable(null)
}
}
title.setOnClickListener { clicked(ListenerType.Page(item.id)) }

View file

@ -34,12 +34,16 @@
android:layout_gravity="center"
android:contentDescription="@string/content_description_page_icon" />
<TextView
android:id="@+id/emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/emoji_color" />
<ImageView
android:id="@+id/linkImage"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/linkEmoji"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center" />
</FrameLayout>

View file

@ -14,15 +14,11 @@
android:layout_width="64dp"
android:layout_height="64dp">
<TextView
<ImageView
android:id="@+id/emojiIcon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/rectangle_default_page_logo_background"
android:gravity="center"
android:textColor="@color/emoji_color"
android:textSize="28sp" />
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center" />
<ImageView
android:id="@+id/imageIcon"

View file

@ -152,7 +152,11 @@
<string name="empty_tap_to_create_new_block">Empty. Tap to create a new block</string>
<string name="untitled">Untitled</string>
<string name="loading">Loading…</string>
<string name="error_while_loading">Error while loading</string>
<string name="error_while_loading_picture">Error while \nloading picture</string>
<string name="block_with_a_picture">Block with a picture</string>

View file

@ -1589,7 +1589,8 @@ class BlockAdapterTest {
indent = MockDataFactory.randomInt(),
emoji = null,
isEmpty = MockDataFactory.randomBoolean(),
isArchived = MockDataFactory.randomBoolean()
isArchived = MockDataFactory.randomBoolean(),
image = null
)
val views = listOf(view)
@ -3658,7 +3659,8 @@ class BlockAdapterTest {
id = MockDataFactory.randomString(),
indent = MockDataFactory.randomInt(),
isSelected = false,
emoji = null
emoji = null,
image = null
)
val updated = file.copy(isSelected = true)

View file

@ -608,6 +608,7 @@ class BlockViewDiffUtilTest {
id = id,
indent = MockDataFactory.randomInt(),
emoji = null,
image = null,
isSelected = false
)

View file

@ -8,8 +8,9 @@ import com.agileburo.anytype.domain.common.Id
/**
* Use-case for setting emoji icon.
*/
class SetDocumentEmojiIcon(private val repo: BlockRepository) :
BaseUseCase<Any, SetDocumentEmojiIcon.Params>() {
class SetDocumentEmojiIcon(
private val repo: BlockRepository
) : BaseUseCase<Any, SetDocumentEmojiIcon.Params>() {
override suspend fun run(params: Params) = safe {
repo.setDocumentEmojiIcon(

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
package com.agileburo.anytype.emojifier
object Emojifier {
/**
* cache for [search] results.
*/
private val cache = mutableMapOf<String, Pair<Int, Int>>()
/**
* @param unicode emoji unicode
* @return uri for loading emoji as image
*/
fun uri(unicode: String): String {
val (page, index) = search(unicode)
return uri(page, index)
}
/**
* @param page emoji's page (emoji category)
* @param index emoji's index on the [page]
* @return uri for loading emoji as image
*/
fun uri(page: Int, index: Int): String {
return "file:///android_asset/emoji/${page}_${index}.png"
}
/**
* @param unicode emoji unicode
* @return a pair constisting of emoji's page and emoji's index for this [unicode]
*/
private fun search(unicode: String): Pair<Int, Int> {
val cached = cache[unicode]
if (cached != null) return cached
var result: Pair<Int, Int>? = null
Emoji.data.forEachIndexed { idx, category ->
val index = category.indexOfFirst { emoji -> emoji == unicode }
if (index != -1) {
val pair = Pair(idx, index)
result = pair
cache[unicode] = pair
return@forEachIndexed
}
}
return result ?: throw IllegalStateException("Result not found for: $unicode")
}
}

View file

@ -50,6 +50,7 @@ dependencies {
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-emojifier')
implementation applicationDependencies.appcompat
implementation applicationDependencies.kotlin
@ -61,6 +62,7 @@ dependencies {
implementation applicationDependencies.recyclerView
implementation applicationDependencies.constraintLayout
implementation applicationDependencies.timber
implementation applicationDependencies.glide
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest

View file

@ -5,14 +5,15 @@ import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIco
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_FILTER
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_ITEM
sealed class DocumentEmojiIconPickerView : ViewType {
sealed class EmojiPickerView : ViewType {
/**
* @property alias short name or convenient name for an emoji.
*/
data class Emoji(
val alias: String,
val unicode: String
) : DocumentEmojiIconPickerView() {
val unicode: String,
val page: Int,
val index: Int
) : EmojiPickerView() {
override fun getViewType() = HOLDER_EMOJI_ITEM
}
@ -20,15 +21,15 @@ sealed class DocumentEmojiIconPickerView : ViewType {
* @property category emoji category
*/
data class GroupHeader(
val category: String
) : DocumentEmojiIconPickerView() {
val category: Int
) : EmojiPickerView() {
override fun getViewType() = HOLDER_EMOJI_CATEGORY_HEADER
}
/**
* Emoji filter.
*/
object EmojiFilter : DocumentEmojiIconPickerView() {
object EmojiFilter : EmojiPickerView() {
override fun getViewType() = HOLDER_EMOJI_FILTER
}
}

View file

@ -3,8 +3,8 @@ package com.agileburo.anytype.library_page_icon_picker_widget.model
import androidx.recyclerview.widget.DiffUtil
class PageIconPickerViewDiffUtil(
private val old: List<DocumentEmojiIconPickerView>,
private val new: List<DocumentEmojiIconPickerView>
private val old: List<EmojiPickerView>,
private val new: List<EmojiPickerView>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {

View file

@ -6,7 +6,7 @@ import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.agileburo.anytype.library_page_icon_picker_widget.R
import com.agileburo.anytype.library_page_icon_picker_widget.model.DocumentEmojiIconPickerView
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerViewDiffUtil
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_CATEGORY_HEADER
import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerViewHolder.Companion.HOLDER_EMOJI_FILTER
@ -14,9 +14,9 @@ import com.agileburo.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIco
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_filter.view.*
class DocumentEmojiIconPickerAdapter(
private var views: List<DocumentEmojiIconPickerView>,
private var views: List<EmojiPickerView>,
private val onFilterQueryChanged: (String) -> Unit,
private val onEmojiClicked: (String, String) -> Unit
private val onEmojiClicked: (String) -> Unit
) : RecyclerView.Adapter<DocumentEmojiIconPickerViewHolder>() {
override fun onCreateViewHolder(
@ -61,18 +61,18 @@ class DocumentEmojiIconPickerAdapter(
override fun onBindViewHolder(holder: DocumentEmojiIconPickerViewHolder, position: Int) {
when (holder) {
is DocumentEmojiIconPickerViewHolder.CategoryHeader -> {
holder.bind(views[position] as DocumentEmojiIconPickerView.GroupHeader)
holder.bind(views[position] as EmojiPickerView.GroupHeader)
}
is DocumentEmojiIconPickerViewHolder.EmojiItem -> {
holder.bind(
item = views[position] as DocumentEmojiIconPickerView.Emoji,
item = views[position] as EmojiPickerView.Emoji,
onEmojiClicked = onEmojiClicked
)
}
}
}
fun update(update: List<DocumentEmojiIconPickerView>) {
fun update(update: List<EmojiPickerView>) {
val result = DiffUtil.calculateDiff(
PageIconPickerViewDiffUtil(
old = views,

View file

@ -2,7 +2,12 @@ package com.agileburo.anytype.library_page_icon_picker_widget.ui
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.agileburo.anytype.library_page_icon_picker_widget.model.DocumentEmojiIconPickerView
import com.agileburo.anytype.emojifier.Emoji
import com.agileburo.anytype.emojifier.Emojifier
import com.agileburo.anytype.library_page_icon_picker_widget.R
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_category_header.view.*
import kotlinx.android.synthetic.main.item_page_icon_picker_emoji_item.view.*
@ -12,22 +17,36 @@ sealed class DocumentEmojiIconPickerViewHolder(view: View) : RecyclerView.ViewHo
private val category = itemView.category
fun bind(item: DocumentEmojiIconPickerView.GroupHeader) {
category.text = item.category
fun bind(item: EmojiPickerView.GroupHeader) {
when (item.category) {
Emoji.CATEGORY_SMILEYS_AND_PEOPLE -> category.setText(R.string.category_smileys_and_people)
Emoji.CATEGORY_ANIMALS_AND_NATURE -> category.setText(R.string.category_animals_and_nature)
Emoji.CATEGORY_FOOD_AND_DRINK -> category.setText(R.string.category_food_and_drink)
Emoji.CATEGORY_ACTIVITY_AND_SPORT -> category.setText(R.string.category_activity_and_sport)
Emoji.CATEGORY_TRAVEL_AND_PLACES -> category.setText(R.string.category_travel_and_places)
Emoji.CATEGORY_OBJECTS -> category.setText(R.string.category_objects)
Emoji.CATEGORY_SYMBOLS -> category.setText(R.string.category_symbols)
Emoji.CATEGORY_FLAGS -> category.setText(R.string.category_flags)
}
}
}
class EmojiItem(view: View) : DocumentEmojiIconPickerViewHolder(view) {
private val emoji = itemView.emoji
private val image = itemView.image
fun bind(
item: DocumentEmojiIconPickerView.Emoji,
onEmojiClicked: (String, String) -> Unit
item: EmojiPickerView.Emoji,
onEmojiClicked: (String) -> Unit
) {
emoji.text = item.unicode
itemView.setOnClickListener { onEmojiClicked(item.unicode, item.alias) }
Glide
.with(image)
.load(Emojifier.uri(item.page, item.index))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(image)
itemView.setOnClickListener { onEmojiClicked(item.unicode) }
}
}

View file

@ -1,11 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:gravity="center"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textAllCaps="true"
android:id="@+id/category"
android:layout_width="wrap_content"
tools:text="Smileys and people"
android:layout_height="wrap_content"
android:fontFamily="@font/graphik_medium"
android:textColor="@color/emoji_category_text_color"

View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="56dp">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/emoji"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/emoji_color"
android:textSize="22sp"
android:gravity="center"
tools:text="🎢" />
<ImageView
android:id="@+id/image"
android:layout_gravity="center"
android:layout_width="40dp"
android:layout_height="40dp" />
</FrameLayout>

View file

@ -8,4 +8,15 @@
<string name="content_description_loop_icon">Loop icon</string>
<string name="page_icon">Page icon</string>
<string name="page_icon_picker_remove_text">Remove</string>
<string name="category_smileys_and_people">Smileys &amp; People</string>
<string name="category_animals_and_nature">Animals &amp; Nature</string>
<string name="category_food_and_drink">Food &amp; Drink</string>
<string name="category_activity_and_sport">Activity &amp; Sport</string>
<string name="category_travel_and_places">Travel &amp; Places</string>
<string name="category_objects">Objects</string>
<string name="category_symbols">Symbols</string>
<string name="category_flags">Flags</string>
</resources>

View file

@ -1,6 +1,6 @@
package com.agileburo.anytype.library_page_icon_picker_widget
import com.agileburo.anytype.library_page_icon_picker_widget.model.DocumentEmojiIconPickerView
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
import com.agileburo.anytype.library_page_icon_picker_widget.model.PageIconPickerViewDiffUtil
import org.junit.Test
import kotlin.test.assertEquals
@ -10,11 +10,11 @@ class DocumentEmojiIconPickerViewDiffUtilTest {
@Test
fun `two emoji-filter items should be considered the same`() {
val old = listOf(
DocumentEmojiIconPickerView.EmojiFilter
EmojiPickerView.EmojiFilter
)
val new = listOf(
DocumentEmojiIconPickerView.EmojiFilter
EmojiPickerView.EmojiFilter
)
val util = PageIconPickerViewDiffUtil(
@ -32,17 +32,23 @@ class DocumentEmojiIconPickerViewDiffUtilTest {
@Test
fun `two emoji items should be considered the same`() {
val page = 5
val index = 5
val old = listOf(
DocumentEmojiIconPickerView.Emoji(
alias = "grining",
unicode = "U+13131"
EmojiPickerView.Emoji(
unicode = "U+13131",
page = page,
index = index
)
)
val new = listOf(
DocumentEmojiIconPickerView.Emoji(
alias = "grining",
unicode = "U+13131"
EmojiPickerView.Emoji(
unicode = "U+13131",
page = page,
index = index
)
)
@ -58,33 +64,4 @@ class DocumentEmojiIconPickerViewDiffUtilTest {
actual = result
)
}
@Test
fun `two emoji items should be considered different`() {
val old = listOf(
DocumentEmojiIconPickerView.Emoji(
alias = "smile",
unicode = "U+13131"
)
)
val new = listOf(
DocumentEmojiIconPickerView.Emoji(
alias = "grining",
unicode = "U+13131"
)
)
val util = PageIconPickerViewDiffUtil(
old = old,
new = new
)
val result = util.areItemsTheSame(0, 0)
assertEquals(
expected = false,
actual = result
)
}
}

View file

@ -27,6 +27,7 @@ dependencies {
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-page-icon-picker-widget')
implementation project(':library-emojifier')
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting

View file

@ -0,0 +1,83 @@
package com.agileburo.anytype.presentation.page.picker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.emojifier.Emoji
import com.agileburo.anytype.library_page_icon_picker_widget.model.EmojiPickerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class DocumentEmojiIconPickerViewModel(
private val setEmojiIcon: SetDocumentEmojiIcon
) : ViewModel() {
private val state: MutableStateFlow<ViewState> = MutableStateFlow(ViewState.Init)
init {
viewModelScope.launch {
state.value = ViewState.Loading
state.value = ViewState.Success(views = load())
}
}
fun state(): StateFlow<ViewState> = state
private suspend fun load(): List<EmojiPickerView> = withContext(Dispatchers.IO) {
val views = mutableListOf<EmojiPickerView>()
views.add(EmojiPickerView.EmojiFilter)
Emoji.data.forEachIndexed { category, emojis ->
views.add(
EmojiPickerView.GroupHeader(
category = category
)
)
emojis.forEachIndexed { index, unicode ->
val skin = Emoji.colors.any { color -> unicode.contains(color) }
if (!skin) {
views.add(
EmojiPickerView.Emoji(
unicode = unicode,
page = category,
index = index
)
)
}
}
}
views
}
fun onEmojiClicked(unicode: String, target: Id, context: Id) {
viewModelScope.launch {
setEmojiIcon(
params = SetDocumentEmojiIcon.Params(
emoji = unicode,
target = target,
context = context
)
).proceed(
failure = { Timber.e(it, "Error while setting emoji") },
success = { state.apply { value = ViewState.Exit } }
)
}
}
sealed class ViewState {
object Init : ViewState()
object Loading : ViewState()
data class Success(val views: List<EmojiPickerView>) : ViewState()
object Exit : ViewState()
}
}

View file

@ -0,0 +1,17 @@
package com.agileburo.anytype.presentation.page.picker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
class DocumentEmojiIconPickerViewModelFactory(
private val setEmojiIcon: SetDocumentEmojiIcon
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DocumentEmojiIconPickerViewModel(
setEmojiIcon = setEmojiIcon
) as T
}
}

View file

@ -0,0 +1,225 @@
package com.agileburo.anytype.presentation.page.picker
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.domain.icon.SetDocumentImageIcon
import com.agileburo.anytype.emojifier.Emoji
import com.agileburo.anytype.presentation.common.StateReducer
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.Contract.*
import com.agileburo.anytype.presentation.page.picker.DocumentIconActionMenuViewModel.ViewState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class DocumentIconActionMenuViewModel(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val setImageIcon: SetDocumentImageIcon
) : ViewStateViewModel<ViewState>(), StateReducer<State, Event> {
private val events = ConflatedBroadcastChannel<Event>()
private val actions = Channel<Action>()
private val flow: Flow<State> = events.asFlow().scan(State.init(), function)
override val function: suspend (State, Event) -> State
get() = { state, event -> reduce(state, event) }
init {
flow
.map { state ->
when {
state.error != null -> ViewState.Error(state.error)
state.isCompleted -> ViewState.Exit
else -> ViewState.Idle
}
}
.onEach { stateData.postValue(it) }
.launchIn(viewModelScope)
actions
.consumeAsFlow()
.onEach { action ->
when (action) {
is Action.SetEmojiIcon -> setEmojiIcon(
params = SetDocumentEmojiIcon.Params(
target = action.target,
emoji = action.unicode,
context = action.context
)
).proceed(
success = { events.send(Event.OnCompleted) },
failure = { events.send(Event.Failure(it)) }
)
is Action.ClearEmoji -> setEmojiIcon(
params = SetDocumentEmojiIcon.Params(
target = action.target,
emoji = "",
context = action.context
)
).proceed(
success = { events.send(Event.OnCompleted) },
failure = { events.send(Event.Failure(it)) }
)
is Action.SetImageIcon -> setImageIcon(
SetDocumentImageIcon.Params(
context = action.context,
path = action.path
)
).proceed(
failure = { events.send(Event.Failure(it)) },
success = { events.send(Event.OnCompleted) }
)
is Action.PickRandomEmoji -> {
val random = Emoji.data.random().random()
events.send(
Event.OnRandomEmojiSelected(
target = action.target,
context = action.context,
unicode = random
)
)
}
}
}
.launchIn(viewModelScope)
}
fun onEvent(event: Event) {
viewModelScope.launch { events.send(event) }
}
sealed class ViewState {
object Loading : ViewState()
object Exit : ViewState()
object Idle : ViewState()
data class Error(val message: String) : ViewState()
}
sealed class Contract {
sealed class Action {
class PickRandomEmoji(
val context: String,
val target: String
) : Action()
class ClearEmoji(
val target: String,
val context: String
) : Action()
class SetEmojiIcon(
val unicode: String,
val target: String,
val context: String
) : Action()
class SetImageIcon(
val context: String,
val path: String
) : Action()
}
data class State(
val isLoading: Boolean,
val isCompleted: Boolean = false,
val error: String? = null
) : Contract() {
companion object {
fun init() = State(isLoading = false)
}
}
sealed class Event : Contract() {
class OnImagePickedFromGallery(
val context: String,
val path: String
) : Event()
class OnSetRandomEmojiClicked(
val target: String,
val context: String
) : Event()
class OnRandomEmojiSelected(
val unicode: String,
val context: String,
val target: String
) : Event()
class OnRemoveEmojiSelected(
val context: String,
val target: String
) : Event()
object OnCompleted : Event()
class Failure(val error: Throwable) : Event()
}
}
override suspend fun reduce(state: State, event: Event): State {
return when (event) {
is Event.OnRandomEmojiSelected -> state.copy(
isLoading = true
).also {
actions.send(
Action.SetEmojiIcon(
target = event.target,
context = event.context,
unicode = event.unicode
)
)
}
is Event.OnSetRandomEmojiClicked -> {
state.copy(
isLoading = true
).also {
actions.send(
Action.PickRandomEmoji(
target = event.target,
context = event.context
)
)
}
}
is Event.OnRemoveEmojiSelected -> {
state.copy(
isLoading = true
).also {
actions.send(
Action.ClearEmoji(
context = event.context,
target = event.target
)
)
}
}
is Event.OnImagePickedFromGallery -> {
state.copy(
isLoading = true
).also {
actions.send(
Action.SetImageIcon(
context = event.context,
path = event.path
)
)
}
}
is Event.OnCompleted -> state.copy(
isLoading = false,
isCompleted = true,
error = null
)
is Event.Failure -> state.copy(
isLoading = false,
isCompleted = false,
error = event.error.toString()
)
}
}
}

View file

@ -5,13 +5,13 @@ import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.domain.icon.SetDocumentImageIcon
class DocumentIconPickerViewModelFactory(
class DocumentIconActionMenuViewModelFactory(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val setImageIcon: SetDocumentImageIcon
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = DocumentIconPickerViewModel(
override fun <T : ViewModel?> create(modelClass: Class<T>): T = DocumentIconActionMenuViewModel(
setEmojiIcon = setEmojiIcon,
setImageIcon = setImageIcon
) as T

View file

@ -1,371 +0,0 @@
package com.agileburo.anytype.presentation.page.picker
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.icon.SetDocumentEmojiIcon
import com.agileburo.anytype.domain.icon.SetDocumentImageIcon
import com.agileburo.anytype.library_page_icon_picker_widget.model.DocumentEmojiIconPickerView
import com.agileburo.anytype.presentation.common.StateReducer
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel.Contract.Event
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel.Contract.State
import com.agileburo.anytype.presentation.page.picker.DocumentIconPickerViewModel.ViewState
import com.vdurmont.emoji.Emoji
import com.vdurmont.emoji.EmojiManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class DocumentIconPickerViewModel(
private val setEmojiIcon: SetDocumentEmojiIcon,
private val setImageIcon: SetDocumentImageIcon
) : ViewStateViewModel<ViewState>(), StateReducer<State, Event> {
private val channel = ConflatedBroadcastChannel<Event>()
private val actions = Channel<Contract.Action>()
private val flow: Flow<State> = channel.asFlow().scan(State.init(), function)
override val function: suspend (State, Event) -> State
get() = { state, event -> reduce(state, event) }
private val headers = listOf(
DocumentEmojiIconPickerView.EmojiFilter
)
init {
processViewState()
processActions()
initialize()
}
private fun initialize() {
channel.offer(Event.Init)
}
private fun processViewState() {
flow
.map { state ->
when {
state.error != null -> ViewState.Error(state.error)
state.isCompleted -> ViewState.Exit
else -> ViewState.Success(
views = headers + map(state.selection)
)
}
}
.onEach { stateData.postValue(it) }
.launchIn(viewModelScope)
}
private fun processActions() {
actions
.consumeAsFlow()
.map { action ->
when (action) {
is Contract.Action.FetchEmojis -> {
val emojis = loadEmoji()
Event.OnEmojiLoaded(emojis)
}
is Contract.Action.SearchEmojis -> {
val emojis = findEmojis(action.query)
Event.OnSearchResult(emojis)
}
is Contract.Action.SetEmoji -> {
setIconEmojiName(action).let { result ->
when (result) {
is Either.Left -> Event.Failure(result.a)
is Either.Right -> Event.OnCompleted
}
}
}
is Contract.Action.ClearEmoji -> {
clearIconEmojiName(action).let { result ->
when (result) {
is Either.Left -> Event.Failure(result.a)
is Either.Right -> Event.OnCompleted
}
}
}
is Contract.Action.PickRandomEmoji -> {
val emoji = pickRandomEmoji(action.emojis)
Event.OnRandomEmojiSelected(
target = action.target,
context = action.context,
emoji = emoji
)
}
}
}
.onEach(channel::send)
.launchIn(viewModelScope)
}
private suspend fun setIconEmojiName(
action: Contract.Action.SetEmoji
): Either<Throwable, Unit> = withContext(Dispatchers.IO) {
setEmojiIcon.run(
params = SetDocumentEmojiIcon.Params(
target = action.target,
emoji = action.unicode,
context = action.context
)
)
}
fun onImagePickedFromGallery(context: String, path: String) {
viewModelScope.launch {
setImageIcon(
SetDocumentImageIcon.Params(
context = context,
path = path
)
).proceed(
failure = { Timber.e(it, "Error while setting document image icon") },
success = { stateData.postValue(ViewState.Exit) }
)
}
}
private suspend fun clearIconEmojiName(
action: Contract.Action.ClearEmoji
): Either<Throwable, Unit> = withContext(Dispatchers.IO) {
setEmojiIcon.run(
params = SetDocumentEmojiIcon.Params(
target = action.target,
emoji = "",
context = action.context
)
)
}
private suspend fun loadEmoji(): List<Emoji> = withContext(Dispatchers.IO) {
EmojiManager.getAll().toList()
}
private suspend fun findEmojis(query: String): List<Emoji> = withContext(Dispatchers.IO) {
EmojiManager.getAll().filter { emoji ->
emoji.aliases.any { alias ->
alias.contains(query, ignoreCase = true) || query == alias
}
}
}
private suspend fun pickRandomEmoji(emojis: List<Emoji>): Emoji = withContext(Dispatchers.IO) {
EmojiManager.getAll().random()
}
private suspend fun map(emojis: List<Emoji>) = withContext(Dispatchers.IO) {
emojis.map { emoji ->
DocumentEmojiIconPickerView.Emoji(
alias = emoji.aliases.first(),
/**
* Fix pirate flag emoji render, after fixing
* in table https://github.com/vdurmont/emoji-java/blob/master/EMOJIS.md
* can be removed
*/
unicode = emoji.unicode.filterTextByChar(
value = '☠',
filterBy = '♾'
)
)
}
}
private fun String.filterTextByChar(value: Char, filterBy: Char): String =
if (contains(value)) {
filterNot { it == filterBy }
} else {
this
}
fun onEvent(event: Event) {
channel.offer(event)
}
sealed class ViewState {
object Loading : ViewState()
object Exit : ViewState()
data class Success(val views: List<DocumentEmojiIconPickerView>) : ViewState()
data class Error(val message: String) : ViewState()
}
sealed class Contract {
sealed class Action {
object FetchEmojis : Action()
data class SearchEmojis(
val query: String
) : Action()
data class PickRandomEmoji(
val emojis: List<Emoji>,
val context: String,
val target: String
) : Action()
data class ClearEmoji(
val target: String,
val context: String
) : Action()
data class SetEmoji(
val unicode: String,
val alias: String,
val target: String,
val context: String
) : Action()
}
data class State(
val isLoading: Boolean,
val isCompleted: Boolean = false,
val error: String? = null,
val emojis: List<Emoji>,
val selection: List<Emoji>
) : Contract() {
companion object {
fun init() = State(
isLoading = false,
emojis = emptyList(),
selection = emptyList()
)
}
}
sealed class Event : Contract() {
object Init : Event()
data class OnEmojiClicked(
val unicode: String,
val alias: String,
val target: String,
val context: String
) : Event()
data class OnFilterQueryChanged(
val query: String
) : Event()
data class OnSetRandomEmojiClicked(
val target: String,
val context: String
) : Event()
data class OnEmojiLoaded(
val emojis: List<Emoji>
) : Event()
data class OnRandomEmojiSelected(
val emoji: Emoji,
val context: String,
val target: String
) : Event()
data class OnRemoveEmojiSelected(
val context: String,
val target: String
) : Event()
data class OnSearchResult(
val emojis: List<Emoji>
) : Event()
object OnCompleted : Event()
data class Failure(val error: Throwable) : Event()
}
}
override suspend fun reduce(state: State, event: Event): State {
return when (event) {
is Event.Init -> state.copy(isLoading = true).also {
actions.send(Contract.Action.FetchEmojis)
}
is Event.OnEmojiLoaded -> state.copy(
isLoading = false,
emojis = event.emojis,
selection = event.emojis
)
is Event.OnSearchResult -> state.copy(
isLoading = false,
selection = event.emojis
)
is Event.OnRandomEmojiSelected -> state.copy(
isLoading = true
).also {
actions.send(
Contract.Action.SetEmoji(
target = event.target,
context = event.context,
unicode = event.emoji.unicode,
alias = event.emoji.aliases.first()
)
)
}
is Event.OnFilterQueryChanged -> state.copy(
isLoading = true
).also {
actions.send(
Contract.Action.SearchEmojis(
query = event.query
)
)
}
is Event.OnEmojiClicked -> {
state.copy(isLoading = true).also {
actions.send(
Contract.Action.SetEmoji(
unicode = event.unicode,
target = event.target,
alias = event.alias,
context = event.context
)
)
}
}
is Event.OnSetRandomEmojiClicked -> {
state.copy(
isLoading = true
).also {
actions.send(
Contract.Action.PickRandomEmoji(
emojis = state.emojis,
target = event.target,
context = event.context
)
)
}
}
is Event.OnRemoveEmojiSelected -> {
state.copy(
isLoading = true
).also {
actions.send(
Contract.Action.ClearEmoji(
context = event.context,
target = event.target
)
)
}
}
is Event.OnCompleted -> state.copy(
isLoading = false,
isCompleted = true,
error = null
)
is Event.Failure -> state.copy(
isLoading = false,
isCompleted = false,
error = event.error.toString()
)
}
}
}

View file

@ -477,6 +477,12 @@ class DefaultBlockViewRenderer(
else
null
},
image = details.details[content.target]?.iconImage?.let { name ->
if (name.isNotEmpty())
urlBuilder.image(name)
else
null
},
text = details.details[content.target]?.name,
indent = indent
)