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:
parent
7c32a77909
commit
45a6430c70
39 changed files with 19861 additions and 556 deletions
15998
app/src/main/assets/emoji_suggestions.json
Normal file
15998
app/src/main/assets/emoji_suggestions.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -44,6 +44,7 @@ dependencies {
|
|||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
|
||||
implementation project(':core-utils')
|
||||
implementation project(':library-emojifier')
|
||||
|
||||
implementation applicationDependencies.appcompat
|
||||
implementation applicationDependencies.kotlin
|
||||
|
|
|
@ -96,8 +96,6 @@ class GoalAdapter(
|
|||
|
||||
fun bind(item: GoalView.Title) {
|
||||
title.setText(item.title)
|
||||
logo.text = "⛳"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)) }
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -608,6 +608,7 @@ class BlockViewDiffUtilTest {
|
|||
id = id,
|
||||
indent = MockDataFactory.randomInt(),
|
||||
emoji = null,
|
||||
image = null,
|
||||
isSelected = false
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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 & People</string>
|
||||
<string name="category_animals_and_nature">Animals & Nature</string>
|
||||
<string name="category_food_and_drink">Food & Drink</string>
|
||||
<string name="category_activity_and_sport">Activity & Sport</string>
|
||||
<string name="category_travel_and_places">Travel & Places</string>
|
||||
<string name="category_objects">Objects</string>
|
||||
<string name="category_symbols">Symbols</string>
|
||||
<string name="category_flags">Flags</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue