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

Add bookmark block (#292)

This commit is contained in:
Evgenii Kozlov 2020-03-17 16:00:50 +01:00 committed by GitHub
parent 0d1747283c
commit ea304c62df
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1111 additions and 390 deletions

View file

@ -1,5 +1,20 @@
# Change log for Android @Anytype app.
## Version 0.0.24 (WIP)
### New features 🚀
* User can add bookmark placeholder and create bookmark from url (#140)
### Fixes & tech 🚒
* Refactored block creation in `Middleware` (introduced factory to create a block from a block prototype) (#140)
* New mappers (from middleware layer entity to data layer entity) (#140)
### Middleware ⚙️
* Added `blockBookmarkFetch` command (#140)
## Version 0.0.23
### New features 🚀

View file

@ -122,6 +122,13 @@ class ComponentManager(private val main: MainComponent) {
.build()
}
val createBookmarkSubComponent = Component {
main
.createBookmarkBuilder()
.createBookmarkModule(CreateBookmarkModule())
.build()
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -0,0 +1,45 @@
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.page.bookmark.SetupBookmark
import com.agileburo.anytype.presentation.page.bookmark.CreateBookmarkViewModel
import com.agileburo.anytype.ui.page.modals.CreateBookmarkFragment
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(modules = [CreateBookmarkModule::class])
@PerScreen
interface CreateBookmarkSubComponent {
@Subcomponent.Builder
interface Builder {
fun createBookmarkModule(module: CreateBookmarkModule): Builder
fun build(): CreateBookmarkSubComponent
}
fun inject(fragment: CreateBookmarkFragment)
}
@Module
class CreateBookmarkModule {
@Provides
@PerScreen
fun provideCreateBookmarkViewModelFactory(
setupBookmark: SetupBookmark
): CreateBookmarkViewModel.Factory {
return CreateBookmarkViewModel.Factory(
setupBookmark = setupBookmark
)
}
@Provides
@PerScreen
fun provideSetBookmarkUrlUseCase(
repo: BlockRepository
): SetupBookmark = SetupBookmark(
repo = repo
)
}

View file

@ -16,6 +16,8 @@ import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.auth.AuthMiddleware
import com.agileburo.anytype.middleware.block.BlockMiddleware
import com.agileburo.anytype.middleware.interactor.Middleware
import com.agileburo.anytype.middleware.interactor.MiddlewareFactory
import com.agileburo.anytype.middleware.interactor.MiddlewareMapper
import com.agileburo.anytype.middleware.service.DefaultMiddlewareService
import com.agileburo.anytype.middleware.service.MiddlewareService
import com.agileburo.anytype.persistence.db.AnytypeDatabase
@ -149,8 +151,18 @@ class DataModule {
@Provides
@Singleton
fun provideMiddleware(
service: MiddlewareService
): Middleware = Middleware(service)
service: MiddlewareService,
factory: MiddlewareFactory,
mapper: MiddlewareMapper
): Middleware = Middleware(service, factory, mapper)
@Provides
@Singleton
fun provideMiddlewareFactory(): MiddlewareFactory = MiddlewareFactory()
@Provides
@Singleton
fun provideMiddlewareMapper(): MiddlewareMapper = MiddlewareMapper()
@Provides
@Singleton

View file

@ -34,4 +34,5 @@ interface MainComponent {
fun pageComponentBuilder(): PageSubComponent.Builder
fun linkAddComponentBuilder(): LinkSubComponent.Builder
fun pageIconPickerBuilder(): PageIconPickerSubComponent.Builder
fun createBookmarkBuilder(): CreateBookmarkSubComponent.Builder
}

View file

@ -31,6 +31,7 @@ import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.Option
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_LIST_BULLETED_LIST
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_LIST_CHECKBOX
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_LIST_NUMBERED_LIST
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_MEDIA_BOOKMARK
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_MEDIA_VIDEO
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_HEADER_ONE
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_HEADER_THREE
@ -49,6 +50,7 @@ import com.agileburo.anytype.ext.extractMarks
import com.agileburo.anytype.presentation.page.PageViewModel
import com.agileburo.anytype.presentation.page.PageViewModelFactory
import com.agileburo.anytype.ui.base.NavigationFragment
import com.agileburo.anytype.ui.page.modals.CreateBookmarkFragment
import com.agileburo.anytype.ui.page.modals.PageIconPickerFragment
import com.agileburo.anytype.ui.page.modals.SetLinkFragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -107,7 +109,8 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
onPageIconClicked = vm::onPageIconClicked,
onAddUrlClick = vm::onAddVideoUrlClicked,
onAddLocalVideoClick = vm::onAddLocalVideoClicked,
strVideoError = getString(R.string.error)
strVideoError = getString(R.string.error),
onBookmarkPlaceholderClicked = vm::onBookmarkPlaceholderClicked
)
}
@ -173,8 +176,10 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
wasSuccessful: Boolean,
Reason: String?
) {
Timber.d("PickiTonCompleteListener path:$path, wasDriveFile$wasDriveFile, " +
"wasUnknownProvider:$wasUnknownProvider, wasSuccessful:$wasSuccessful, reason:$Reason")
Timber.d(
"PickiTonCompleteListener path:$path, wasDriveFile$wasDriveFile, " +
"wasUnknownProvider:$wasUnknownProvider, wasSuccessful:$wasSuccessful, reason:$Reason"
)
vm.onAddVideoFileClicked(filePath = path)
}
@ -356,6 +361,7 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
is Option.Media -> {
when (option.type) {
OPTION_MEDIA_VIDEO -> vm.onAddVideoBlockClicked()
OPTION_MEDIA_BOOKMARK -> vm.onAddBookmarkClicked()
else -> toast(NOT_IMPLEMENTED_MESSAGE)
}
}
@ -443,6 +449,12 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenBookmarkSetter -> {
CreateBookmarkFragment.newInstance(
context = command.context,
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenGallery -> {
openGalleryWithPermissionCheck(command.mediaType)
}

View file

@ -0,0 +1,108 @@
package com.agileburo.anytype.ui.page.modals
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
import com.agileburo.anytype.R
import com.agileburo.anytype.core_ui.reactive.clicks
import com.agileburo.anytype.core_utils.ext.toast
import com.agileburo.anytype.core_utils.ui.BaseBottomSheetFragment
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.presentation.page.bookmark.CreateBookmarkViewModel
import com.agileburo.anytype.presentation.page.bookmark.CreateBookmarkViewModel.ViewState
import kotlinx.android.synthetic.main.dialog_create_bookmark.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class CreateBookmarkFragment : BaseBottomSheetFragment(), Observer<ViewState> {
private val target: String
get() = requireArguments()
.getString(ARG_TARGET)
?: throw IllegalStateException(MISSING_TARGET_ERROR)
private val context: String
get() = requireArguments()
.getString(ARG_CONTEXT)
?: throw IllegalStateException(MISSING_CONTEXT_ERROR)
@Inject
lateinit var factory: CreateBookmarkViewModel.Factory
private val vm by lazy {
ViewModelProviders
.of(this, factory)
.get(CreateBookmarkViewModel::class.java)
}
companion object {
private const val ARG_CONTEXT = "arg.create.bookmark.context"
private const val ARG_TARGET = "arg.create.bookmark.target"
private const val MISSING_TARGET_ERROR = "Target missing in args"
private const val MISSING_CONTEXT_ERROR = "Context missing in args"
fun newInstance(
context: String,
target: String
): CreateBookmarkFragment = CreateBookmarkFragment().apply {
arguments = bundleOf(
ARG_CONTEXT to context,
ARG_TARGET to target
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogStyle)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.dialog_create_bookmark, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
createBookmarkButton
.clicks()
.onEach {
vm.onCreateBookmarkClicked(
context = context,
target = target,
url = urlInput.text.toString()
)
}
.launchIn(lifecycleScope)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
vm.state.observe(viewLifecycleOwner, this)
}
override fun onChanged(state: ViewState) {
if (state is ViewState.Exit)
dismiss()
else if (state is ViewState.Error)
toast(state.message)
}
override fun injectDependencies() {
componentManager().createBookmarkSubComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().createBookmarkSubComponent.release()
}
}

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/modal_rect_margin_top"
android:contentDescription="@string/content_description_modal_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sheet_top" />
<EditText
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="@null"
android:hint="@string/hint_paste_or_type_a_url"
android:inputType="textUri"
android:textColor="@color/black"
android:textColorHint="@color/hint_color"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/drag" />
<TextView
android:id="@+id/createBookmarkButton"
style="@style/DefaultSolidButtonStyle"
android:layout_width="0dp"
android:layout_height="@dimen/auth_default_button_height"
android:layout_marginStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text="@string/create_bookmark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="20dp"
android:background="@color/divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/urlInput" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,5 +15,6 @@
<color name="blue">#3F51B5</color>
<color name="auth_divider">#DFDDD0</color>
<color name="hint_color">#ACA996</color>
</resources>

View file

@ -100,5 +100,7 @@ Do the computation of an expensive paragraph of text on a background thread:
<string name="page_icon">Page icon</string>
<string name="page_icon_picker_remove_text">Remove</string>
<string name="create_bookmark">Create bookmark</string>
<string name="hint_paste_or_type_a_url">Paste or type a URL</string>
</resources>

View file

@ -7,6 +7,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.agileburo.anytype.core_ui.R
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BOOKMARK
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BOOKMARK_PLACEHOLDER
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BULLET
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_CHECKBOX
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_CODE_SNIPPET
@ -53,10 +54,11 @@ class BlockAdapter(
private val onPageClicked: (String) -> Unit,
private val onTextInputClicked: () -> Unit,
private val onAddUrlClick: (String, String) -> Unit,
private val onAddLocalVideoClick : (String) -> Unit,
private val onAddLocalVideoClick: (String) -> Unit,
private val strVideoError: String,
private val onPageIconClicked: () -> Unit,
private val onDownloadFileClicked: (String) -> Unit
private val onDownloadFileClicked: (String) -> Unit,
private val onBookmarkPlaceholderClicked: (String) -> Unit
) : RecyclerView.Adapter<BlockViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder {
@ -235,6 +237,15 @@ class BlockAdapter(
)
)
}
HOLDER_BOOKMARK_PLACEHOLDER -> {
BlockViewHolder.Bookmark.Placeholder(
view = inflater.inflate(
R.layout.item_block_bookmark_placeholder,
parent,
false
)
)
}
HOLDER_PICTURE -> {
BlockViewHolder.Picture(
view = inflater.inflate(
@ -454,7 +465,13 @@ class BlockAdapter(
}
is BlockViewHolder.Bookmark -> {
holder.bind(
item = blocks[position] as BlockView.Bookmark
item = blocks[position] as BlockView.Bookmark.View
)
}
is BlockViewHolder.Bookmark.Placeholder -> {
holder.bind(
item = blocks[position] as BlockView.Bookmark.Placeholder,
onBookmarkPlaceholderClicked = onBookmarkPlaceholderClicked
)
}
is BlockViewHolder.Picture -> {

View file

@ -5,6 +5,7 @@ import com.agileburo.anytype.core_ui.common.Focusable
import com.agileburo.anytype.core_ui.common.Markup
import com.agileburo.anytype.core_ui.common.ViewType
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BOOKMARK
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BOOKMARK_PLACEHOLDER
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_BULLET
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_CHECKBOX
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_CODE_SNIPPET
@ -366,21 +367,36 @@ sealed class BlockView : ViewType {
/**
* UI-model for a bookmark block
* @property id block's id
* @property title website's title
* @property title website's content description
* @property url website's url
* @property faviconUrl website's favicon url
* @property imageUrl content's main image url
*/
data class Bookmark(
override val id: String,
val url: String,
val title: String?,
val description: String?,
val faviconUrl: String?,
val imageUrl: String?
sealed class Bookmark(
override val id: String
) : BlockView() {
override fun getViewType() = HOLDER_BOOKMARK
/**
* UI-model for a bookmark placeholder (used when bookmark url is not set)
*/
data class Placeholder(override val id: String) : Bookmark(id = id) {
override fun getViewType() = HOLDER_BOOKMARK_PLACEHOLDER
}
/**
* UI-model for a bookmark view.
* @property title website's title
* @property description website's content description
* @property url website's url
* @property faviconUrl website's favicon url
* @property imageUrl content's main image url
*/
data class View(
override val id: String,
val url: String,
val title: String?,
val description: String?,
val faviconUrl: String?,
val imageUrl: String?
) : Bookmark(id = id) {
override fun getViewType() = HOLDER_BOOKMARK
}
}
/**

View file

@ -1,8 +1,8 @@
package com.agileburo.anytype.core_ui.features.page
import android.graphics.Color
import android.net.Uri
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.Editable
import android.view.View
import android.widget.TextView.BufferType
@ -638,7 +638,6 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
MimeTypes.Category.TABLE -> icon.setImageResource(R.drawable.ic_mime_table)
MimeTypes.Category.PRESENTATION -> icon.setImageResource(R.drawable.ic_mime_presentation)
MimeTypes.Category.OTHER -> icon.setImageResource(R.drawable.ic_mime_other)
}
itemView.setOnClickListener { onDownloadFileClicked(item.id) }
}
@ -741,7 +740,7 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
}
}
fun bind(item: BlockView.Bookmark) {
fun bind(item: BlockView.Bookmark.View) {
title.text = item.title
description.text = item.description
url.text = item.url
@ -759,6 +758,16 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
.into(logo)
}
}
class Placeholder(view: View) : BlockViewHolder(view) {
fun bind(
item: BlockView.Bookmark.Placeholder,
onBookmarkPlaceholderClicked: (String) -> Unit
) {
itemView.setOnClickListener { onBookmarkPlaceholderClicked(item.id) }
}
}
}
class Picture(view: View) : BlockViewHolder(view) {
@ -859,6 +868,7 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
const val HOLDER_VIDEO_UPLOAD = 20
const val HOLDER_VIDEO_EMPTY = 21
const val HOLDER_VIDEO_ERROR = 22
const val HOLDER_BOOKMARK_PLACEHOLDER = 23
const val FOCUS_TIMEOUT_MILLIS = 16L
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M6,14.0323C7.1046,14.0323 8,13.1369 8,12.0323C8,10.9278 7.1046,10.0323 6,10.0323C4.8954,10.0323 4,10.9278 4,12.0323C4,13.1369 4.8954,14.0323 6,14.0323Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,14.0323C13.1046,14.0323 14,13.1369 14,12.0323C14,10.9278 13.1046,10.0323 12,10.0323C10.8954,10.0323 10,10.9278 10,12.0323C10,13.1369 10.8954,14.0323 12,14.0323Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M18,14.0323C19.1046,14.0323 20,13.1369 20,12.0323C20,10.9278 19.1046,10.0323 18,10.0323C16.8954,10.0323 16,10.9278 16,12.0323C16,13.1369 16.8954,14.0323 18,14.0323Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#DFDDD0"
android:fillType="evenOdd"
android:pathData="M12,18L20,22V4C20,2.8954 19.1046,2 18,2H6C4.8954,2 4,2.8954 4,4V22L12,18ZM6,18.7639L12,15.7639L18,18.7639V4H6V18.7639Z" />
</vector>

View file

@ -3,11 +3,11 @@
android:height="24dp"
android:viewportWidth="25"
android:viewportHeight="24">
<path
android:pathData="M20.0029,4H4.0029V20H20.0029V4ZM4.0029,2C2.8984,2 2.0029,2.8954 2.0029,4V20C2.0029,21.1046 2.8984,22 4.0029,22H20.0029C21.1075,22 22.0029,21.1046 22.0029,20V4C22.0029,2.8954 21.1075,2 20.0029,2H4.0029Z"
android:fillColor="#DFDDD0"
android:fillType="evenOdd"/>
<path
android:pathData="M16.9717,11.9696L9.5498,7.6846V16.2546L16.9717,11.9696Z"
android:fillColor="#DFDDD0"/>
<path
android:fillColor="#DFDDD0"
android:fillType="evenOdd"
android:pathData="M20.0029,4H4.0029V20H20.0029V4ZM4.0029,2C2.8984,2 2.0029,2.8954 2.0029,4V20C2.0029,21.1046 2.8984,22 4.0029,22H20.0029C21.1075,22 22.0029,21.1046 22.0029,20V4C22.0029,2.8954 21.1075,2 20.0029,2H4.0029Z" />
<path
android:fillColor="#DFDDD0"
android:pathData="M16.9717,11.9696L9.5498,7.6846V16.2546L16.9717,11.9696Z" />
</vector>

View file

@ -4,6 +4,5 @@
<stroke
android:width="1dp"
android:color="#DFDDD0" />
<corners
android:radius="8dp" />
<corners android:radius="8dp" />
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="#DFDDD0" />
<corners android:radius="4dp" />
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
</shape>

View file

@ -2,10 +2,13 @@
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingStart="@dimen/default_page_item_padding_start"
android:paddingEnd="@dimen/default_page_item_padding_end"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="6dp"
app:cardCornerRadius="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
@ -34,10 +37,10 @@
<TextView
android:id="@+id/bookmarkUrl"
style="@style/BlockBookmarkUrlStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
style="@style/BlockBookmarkUrlStyle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bookmarkLogo"
app:layout_constraintTop_toBottomOf="@+id/bookmarkDescription"
@ -45,12 +48,12 @@
<ImageView
android:id="@+id/bookmarkImage"
android:layout_height="200dp"
android:layout_width="0dp"
app:layout_constraintWidth_default="percent"
android:layout_height="200dp"
android:contentDescription="@string/content_description_bookmark_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
tools:background="@color/black" />
<ImageView
@ -76,5 +79,15 @@
app:layout_constraintStart_toStartOf="@+id/bookmarkImage"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/bookmarkMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_bookmark_menu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="6dp"
android:background="@drawable/rectangle_bookmark_placeholder">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:layout_marginBottom="14dp"
android:src="@drawable/ic_bookmark_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/add_a_web_bookmark"
android:textColor="#ACA996"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_block_more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/default_page_item_padding_start"
@ -32,9 +31,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="24dp"
android:textColor="#ACA996"
android:text="@string/hint_upload"
android:inputType="none"
android:text="@string/hint_upload"
android:textColor="#ACA996"
app:layout_constraintBottom_toBottomOf="@+id/icVideo"
app:layout_constraintEnd_toStartOf="@+id/icMore"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/default_page_item_padding_start"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/default_page_item_padding_start"

View file

@ -127,5 +127,7 @@
<string name="untitled">Untitled</string>
<string name="error_while_loading_picture">Error while loading picture</string>
<string name="block_with_a_picture">Block with a picture</string>
<string name="tap_here_to_insert_a_bookmark_url">Tap here to insert a bookmark url</string>
<string name="add_a_web_bookmark">Add a web bookmark</string>
</resources>

View file

@ -851,7 +851,8 @@ class BlockAdapterTest {
onPageIconClicked = {},
onAddLocalVideoClick = {},
onAddUrlClick = { _, _ -> },
strVideoError = "Error"
strVideoError = "Error",
onBookmarkPlaceholderClicked = {}
)
}
}

View file

@ -7,4 +7,5 @@ import androidx.lifecycle.ViewModel
open class ViewStateViewModel<VS> : ViewModel() {
protected val stateData = MutableLiveData<VS>()
val state: LiveData<VS> = stateData
fun update(update: VS) = stateData.postValue(update)
}

View file

@ -392,6 +392,12 @@ fun Command.UploadVideoBlockUrl.toEntity(): CommandEntity.UploadBlock = CommandE
filePath = filePath
)
fun Command.SetupBookmark.toEntity() = CommandEntity.SetupBookmark(
target = target,
context = context,
url = url
)
fun Position.toEntity(): PositionEntity {
return PositionEntity.valueOf(name)
}
@ -453,6 +459,17 @@ fun EventEntity.toDomain(): Event {
fields = fields?.let { Block.Fields(it.map) }
)
}
is EventEntity.Command.BookmarkGranularChange -> {
Event.Command.BookmarkGranularChange(
context = context,
target = target,
url = url,
title = title,
description = description,
favicon = faviconHash,
image = imageHash
)
}
is EventEntity.Command.UpdateFields -> {
Event.Command.UpdateFields(
context = context,
@ -486,7 +503,8 @@ fun Block.Prototype.toEntity(): BlockEntity.Prototype = when (this) {
style = BlockEntity.Content.Page.Style.valueOf(this.style.name)
)
}
Block.Prototype.Divider -> BlockEntity.Prototype.Divider
is Block.Prototype.Bookmark -> BlockEntity.Prototype.Bookmark
is Block.Prototype.Divider -> BlockEntity.Prototype.Divider
is Block.Prototype.File -> {
BlockEntity.Prototype.File(
type = BlockEntity.Content.File.Type.valueOf(this.type.name),

View file

@ -85,7 +85,7 @@ data class BlockEntity(
}
data class Bookmark(
val url: String,
val url: String?,
val title: String?,
val description: String?,
val image: String?,
@ -104,11 +104,12 @@ data class BlockEntity(
val style: Content.Page.Style
) : Prototype()
object Divider : Prototype()
data class File(
val state: Content.File.State,
val type: Content.File.Type
) : Prototype()
object Divider : Prototype()
object Bookmark : Prototype()
}
}

View file

@ -84,4 +84,10 @@ class CommandEntity {
val target: String,
val name: String
)
data class SetupBookmark(
val context: String,
val target: String,
val url: String
)
}

View file

@ -40,6 +40,16 @@ sealed class EventEntity {
val fields: BlockEntity.Fields?
) : Command()
data class BookmarkGranularChange(
override val context: String,
val target: String,
val url: String?,
val title: String?,
val description: String?,
val imageHash: String?,
val faviconHash: String?
) : Command()
data class UpdateStructure(
override val context: String,
val id: String,

View file

@ -68,8 +68,13 @@ class BlockDataRepository(
override suspend fun split(command: Command.Split) = factory.remote.split(command.toEntity())
override suspend fun setIconName(command: Command.SetIconName) =
factory.remote.setIconName(command.toEntity())
override suspend fun setIconName(
command: Command.SetIconName
) = factory.remote.setIconName(command.toEntity())
override suspend fun setupBookmark(
command: Command.SetupBookmark
) = factory.remote.setupBookmark(command.toEntity())
override suspend fun uploadUrl(command: Command.UploadVideoBlockUrl) {
factory.remote.uploadUrl(command.toEntity())

View file

@ -24,4 +24,5 @@ interface BlockDataStore {
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
suspend fun setIconName(command: CommandEntity.SetIconName)
suspend fun setupBookmark(command: CommandEntity.SetupBookmark)
}

View file

@ -24,4 +24,5 @@ interface BlockRemote {
suspend fun closeDashboard(id: String)
suspend fun setIconName(command: CommandEntity.SetIconName)
suspend fun uploadUrl(command: CommandEntity.UploadBlock)
suspend fun setupBookmark(command: CommandEntity.SetupBookmark)
}

View file

@ -65,6 +65,11 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
override suspend fun split(command: CommandEntity.Split): String = remote.split(command)
override suspend fun setIconName(command: CommandEntity.SetIconName) =
remote.setIconName(command)
override suspend fun setIconName(
command: CommandEntity.SetIconName
) = remote.setIconName(command)
override suspend fun setupBookmark(
command: CommandEntity.SetupBookmark
) = remote.setupBookmark(command)
}

View file

@ -179,7 +179,7 @@ data class Block(
* @property favicon optional hash of bookmark's favicon
*/
data class Bookmark(
val url: Url,
val url: Url?,
val title: String?,
val description: String?,
val image: Hash?,
@ -205,11 +205,12 @@ data class Block(
val style: Content.Page.Style
) : Prototype()
object Divider : Prototype()
data class File(
val type: Content.File.Type,
val state: Content.File.State
) : Prototype()
object Divider : Prototype()
object Bookmark : Prototype()
}
}

View file

@ -147,4 +147,16 @@ sealed class Command {
val target: Id,
val name: String
)
/**
* Command for setting up a bookmark from [url]
* @property context id of the context
* @property target id of the target block (future bookmark block)
* @property url bookmark url
*/
data class SetupBookmark(
val context: Id,
val target: Id,
val url: String
)
}

View file

@ -41,4 +41,6 @@ interface BlockRepository {
suspend fun uploadUrl(command: Command.UploadVideoBlockUrl)
suspend fun setIconName(command: Command.SetIconName)
suspend fun setupBookmark(command: Command.SetupBookmark)
}

View file

@ -2,7 +2,9 @@ package com.agileburo.anytype.domain.event.model
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Block.Content.Text
import com.agileburo.anytype.domain.common.Hash
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.common.Url
sealed class Event {
@ -60,7 +62,7 @@ sealed class Event {
* @property context update's context
* @property id id of the link
* @property target id of the linked block
* @property fields link's fields (considered update if not null)
* @property fields link's fields (considered updated if not null)
*/
data class LinkGranularChange(
override val context: String,
@ -69,6 +71,26 @@ sealed class Event {
val fields: Block.Fields?
) : Command()
/**
* Command to update bookmark
* @property context id of the context
* @property target id of the bookmark block
* @property url bookmark's url (considered updated if not null)
* @property title bookmark's title (considered updated if not null)
* @property description bookmark's description (considered updated if not null)
* @property image bookmark's image hash (considered updated if not null)
* @property favicon bookmark's favicon hash (considered updated if not null)
*/
data class BookmarkGranularChange(
override val context: Id,
val target: Id,
val url: Url?,
val title: String?,
val description: String?,
val image: Hash?,
val favicon: Hash?
) : Command()
/**
* Command to update a block structure.
* @property context context id for this command (i.e page id, dashboard id, etc.)

View file

@ -0,0 +1,41 @@
package com.agileburo.anytype.domain.page.bookmark
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
/**
* Use-case for setting up (i.e. fetching) a bookmark from url.
*/
class SetupBookmark(
private val repo: BlockRepository
) : BaseUseCase<Unit, SetupBookmark.Params>() {
override suspend fun run(params: Params) = try {
repo.setupBookmark(
command = Command.SetupBookmark(
context = params.context,
target = params.target,
url = params.url
)
).let {
Either.Right(it)
}
} catch (t: Throwable) {
Either.Left(t)
}
/**
* Params for setting up a bookmark from [url]
* @property context id of the context
* @property target id of the target block (future bookmark block)
* @property url bookmark url
*/
data class Params(
val context: Id,
val target: Id,
val url: String
)
}

View file

@ -6,6 +6,7 @@ import anytype.model.Models.Account
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.PositionEntity
import com.google.protobuf.Struct
import com.google.protobuf.Value
@ -246,7 +247,7 @@ fun Block.Content.File.State.entity(): BlockEntity.Content.File.State = when (th
}
fun Block.bookmark(): BlockEntity.Content.Bookmark = BlockEntity.Content.Bookmark(
url = bookmark.url,
url = bookmark.url.ifEmpty { null },
description = bookmark.description.ifEmpty { null },
title = bookmark.title.ifEmpty { null },
image = bookmark.imageHash.ifEmpty { null },
@ -345,4 +346,41 @@ fun Block.Content.Text.Style.entity(): BlockEntity.Content.Text.Style = when (th
Block.Content.Text.Style.Toggle -> BlockEntity.Content.Text.Style.TOGGLE
Block.Content.Text.Style.Checkbox -> BlockEntity.Content.Text.Style.CHECKBOX
else -> throw IllegalStateException("Unexpected text style: $this")
}
fun BlockEntity.Content.Text.Style.toMiddleware(): Block.Content.Text.Style = when (this) {
BlockEntity.Content.Text.Style.P -> Block.Content.Text.Style.Paragraph
BlockEntity.Content.Text.Style.H1 -> Block.Content.Text.Style.Header1
BlockEntity.Content.Text.Style.H2 -> Block.Content.Text.Style.Header2
BlockEntity.Content.Text.Style.H3 -> Block.Content.Text.Style.Header3
BlockEntity.Content.Text.Style.TITLE -> Block.Content.Text.Style.Title
BlockEntity.Content.Text.Style.QUOTE -> Block.Content.Text.Style.Quote
BlockEntity.Content.Text.Style.BULLET -> Block.Content.Text.Style.Marked
BlockEntity.Content.Text.Style.NUMBERED -> Block.Content.Text.Style.Numbered
BlockEntity.Content.Text.Style.TOGGLE -> Block.Content.Text.Style.Toggle
BlockEntity.Content.Text.Style.CHECKBOX -> Block.Content.Text.Style.Checkbox
else -> throw IllegalStateException("Unexpected text style: $this")
}
fun BlockEntity.Content.File.State.toMiddleware(): Block.Content.File.State = when (this) {
BlockEntity.Content.File.State.EMPTY -> Block.Content.File.State.Empty
BlockEntity.Content.File.State.ERROR -> Block.Content.File.State.Error
BlockEntity.Content.File.State.UPLOADING -> Block.Content.File.State.Uploading
BlockEntity.Content.File.State.DONE -> Block.Content.File.State.Done
}
fun BlockEntity.Content.File.Type.toMiddleware(): Block.Content.File.Type = when (this) {
BlockEntity.Content.File.Type.NONE -> Block.Content.File.Type.None
BlockEntity.Content.File.Type.FILE -> Block.Content.File.Type.File
BlockEntity.Content.File.Type.IMAGE -> Block.Content.File.Type.Image
BlockEntity.Content.File.Type.VIDEO -> Block.Content.File.Type.Video
}
fun PositionEntity.toMiddleware(): Block.Position = when (this) {
PositionEntity.NONE -> Block.Position.None
PositionEntity.LEFT -> Block.Position.Left
PositionEntity.RIGHT -> Block.Position.Right
PositionEntity.TOP -> Block.Position.Top
PositionEntity.BOTTOM -> Block.Position.Bottom
PositionEntity.INNER -> Block.Position.Inner
}

View file

@ -87,6 +87,11 @@ class BlockMiddleware(
override suspend fun split(command: CommandEntity.Split): String = middleware.split(command)
override suspend fun setIconName(command: CommandEntity.SetIconName) =
middleware.setIconName(command)
override suspend fun setIconName(
command: CommandEntity.SetIconName
) = middleware.setIconName(command)
override suspend fun setupBookmark(
command: CommandEntity.SetupBookmark
) = middleware.setupBookmark(command)
}

View file

@ -22,9 +22,17 @@ import timber.log.Timber;
public class Middleware {
private final MiddlewareService service;
private final MiddlewareFactory factory;
private final MiddlewareMapper mapper;
public Middleware(MiddlewareService service) {
public Middleware(
MiddlewareService service,
MiddlewareFactory factory,
MiddlewareMapper mapper
) {
this.service = service;
this.factory = factory;
this.mapper = mapper;
}
public ConfigEntity getConfig() throws Exception {
@ -219,46 +227,7 @@ public class Middleware {
public void updateTextStyle(CommandEntity.UpdateStyle command) throws Exception {
Models.Block.Content.Text.Style style = null;
switch (command.getStyle()) {
case P:
style = Models.Block.Content.Text.Style.Paragraph;
break;
case H1:
style = Models.Block.Content.Text.Style.Header1;
break;
case H2:
style = Models.Block.Content.Text.Style.Header2;
break;
case H3:
style = Models.Block.Content.Text.Style.Header3;
break;
case H4:
style = Models.Block.Content.Text.Style.Header4;
break;
case TITLE:
style = Models.Block.Content.Text.Style.Title;
break;
case QUOTE:
style = Models.Block.Content.Text.Style.Quote;
break;
case CODE_SNIPPET:
style = Models.Block.Content.Text.Style.Code;
break;
case BULLET:
style = Models.Block.Content.Text.Style.Marked;
break;
case CHECKBOX:
style = Models.Block.Content.Text.Style.Checkbox;
break;
case NUMBERED:
style = Models.Block.Content.Text.Style.Numbered;
break;
case TOGGLE:
style = Models.Block.Content.Text.Style.Toggle;
break;
}
Models.Block.Content.Text.Style style = mapper.toMiddleware(command.getStyle());
Block.Set.Text.Style.Request request = Block.Set.Text.Style.Request
.newBuilder()
@ -304,229 +273,12 @@ public class Middleware {
.setContextId(command.getContextId())
.setBlockId(command.getBlockId())
.build();
Timber.d("Upload video block url with the following request:\n%s", request.toString());
service.blockUpload(request);
}
private Models.Block.Content.Text createTextBlock(
BlockEntity.Content.Text.Style style
) {
Models.Block.Content.Text textBlockModel = null;
switch (style) {
case P:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Paragraph)
.build();
break;
case H1:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Header1)
.build();
break;
case H2:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Header2)
.build();
break;
case H3:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Header3)
.build();
break;
case H4:
throw new IllegalStateException("Unexpected prototype text style");
case TITLE:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Title)
.build();
break;
case QUOTE:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Quote)
.build();
break;
case CODE_SNIPPET:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Code)
.build();
break;
case BULLET:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Marked)
.build();
break;
case CHECKBOX:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Checkbox)
.build();
break;
case NUMBERED:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Numbered)
.build();
break;
case TOGGLE:
textBlockModel = Models.Block.Content.Text
.newBuilder()
.setStyle(Models.Block.Content.Text.Style.Toggle)
.build();
break;
}
return textBlockModel;
}
private Models.Block.Content.Page createPageBlock() {
return Models.Block.Content.Page
.newBuilder()
.setStyle(Models.Block.Content.Page.Style.Empty)
.build();
}
private Models.Block.Content.Div createDivLineBlock() {
return Models.Block.Content.Div
.newBuilder()
.setStyle(Models.Block.Content.Div.Style.Line)
.build();
}
private Models.Block.Content.Div createDivDotsBlock() {
return Models.Block.Content.Div
.newBuilder()
.setStyle(Models.Block.Content.Div.Style.Dots)
.build();
}
private Models.Block.Content.File.State getState(BlockEntity.Content.File.State state) {
switch (state) {
case EMPTY:
return Models.Block.Content.File.State.Empty;
case UPLOADING:
return Models.Block.Content.File.State.Uploading;
case DONE:
return Models.Block.Content.File.State.Done;
case ERROR:
return Models.Block.Content.File.State.Error;
default:
throw new IllegalStateException("Unexpected value: " + state);
}
}
private Models.Block.Content.File.Type getType(BlockEntity.Content.File.Type type) {
switch (type) {
case NONE:
return Models.Block.Content.File.Type.None;
case FILE:
return Models.Block.Content.File.Type.File;
case IMAGE:
return Models.Block.Content.File.Type.Image;
case VIDEO:
return Models.Block.Content.File.Type.Video;
default:
throw new IllegalStateException("Unexpected value: " + type);
}
}
private Models.Block.Content.File createBlock(BlockEntity.Content.File.Type type,
BlockEntity.Content.File.State state) {
return Models.Block.Content.File
.newBuilder()
.setState(getState(state))
.setType(getType(type))
.build();
}
private Models.Block.Position createPosition(PositionEntity position) {
Models.Block.Position positionModel = null;
switch (position) {
case NONE:
positionModel = Models.Block.Position.None;
break;
case TOP:
positionModel = Models.Block.Position.Top;
break;
case BOTTOM:
positionModel = Models.Block.Position.Bottom;
break;
case LEFT:
positionModel = Models.Block.Position.Left;
break;
case RIGHT:
positionModel = Models.Block.Position.Right;
break;
case INNER:
positionModel = Models.Block.Position.Inner;
break;
}
return positionModel;
}
private Models.Block createBlock(Models.Block.Content.Text textBlockModel) {
return Models.Block
.newBuilder()
.setText(textBlockModel)
.build();
}
private Models.Block createBlock(Models.Block.Content.Page pageBlockModel) {
return Models.Block
.newBuilder()
.setPage(pageBlockModel)
.build();
}
private Models.Block createBlock(Models.Block.Content.Div dividerBlockModel) {
return Models.Block
.newBuilder()
.setDiv(dividerBlockModel)
.build();
}
private Models.Block createBlock(Models.Block.Content.File fileBlockModel) {
return Models.Block
.newBuilder()
.setFile(fileBlockModel)
.build();
}
private Models.Block createBlock(Models.Block.Content.Text textBlockModel,
Models.Block.Content.Page pageBlockModel,
Models.Block.Content.Div dividerBlockModel,
Models.Block.Content.File fileBlockModel,
BlockEntity.Prototype prototype) {
Models.Block blockModel = null;
if (textBlockModel != null) {
blockModel = createBlock(textBlockModel);
} else if (pageBlockModel != null) {
blockModel = createBlock(pageBlockModel);
} else if (dividerBlockModel != null) {
blockModel = createBlock(dividerBlockModel);
} else if (fileBlockModel != null) {
blockModel = createBlock(fileBlockModel);
}
if (blockModel == null) {
throw new IllegalStateException("Could not create content from the following prototype: " + prototype.toString());
}
return blockModel;
}
public String createBlock(
String contextId,
String targetId,
@ -534,41 +286,16 @@ public class Middleware {
BlockEntity.Prototype prototype
) throws Exception {
Models.Block.Position positionModel = createPosition(position);
Models.Block.Position positionModel = mapper.toMiddleware(position);
Models.Block.Content.Text textBlockModel = null;
Models.Block.Content.Page pageBlockModel = null;
Models.Block.Content.Div dividerBlockModel = null;
Models.Block.Content.File fileBlockModel = null;
if (prototype instanceof BlockEntity.Prototype.Text) {
textBlockModel = createTextBlock(((BlockEntity.Prototype.Text) prototype).getStyle());
} else if (prototype instanceof BlockEntity.Prototype.Page) {
pageBlockModel = createPageBlock();
} else if (prototype instanceof BlockEntity.Prototype.Divider) {
dividerBlockModel = createDivLineBlock();
} else if (prototype instanceof BlockEntity.Prototype.File) {
fileBlockModel = createBlock(
((BlockEntity.Prototype.File) prototype).getType(),
((BlockEntity.Prototype.File) prototype).getState());
}
Models.Block blockModel = createBlock(textBlockModel, pageBlockModel,
dividerBlockModel, fileBlockModel, prototype);
Models.Block model = factory.create(prototype);
Block.Create.Request request = Block.Create.Request
.newBuilder()
.setContextId(contextId)
.setTargetId(targetId)
.setPosition(positionModel)
.setBlock(blockModel)
.setBlock(model)
.build();
Timber.d("Creating block with the following request:\n%s", request.toString());
@ -579,28 +306,7 @@ public class Middleware {
}
public void dnd(CommandEntity.Dnd command) throws Exception {
Models.Block.Position positionModel = null;
switch (command.getPosition()) {
case NONE:
positionModel = Models.Block.Position.None;
break;
case TOP:
positionModel = Models.Block.Position.Top;
break;
case BOTTOM:
positionModel = Models.Block.Position.Bottom;
break;
case LEFT:
positionModel = Models.Block.Position.Left;
break;
case RIGHT:
positionModel = Models.Block.Position.Right;
break;
case INNER:
positionModel = Models.Block.Position.Inner;
break;
}
Models.Block.Position positionModel = mapper.toMiddleware(command.getPosition());
BlockList.Move.Request request = BlockList.Move.Request
.newBuilder()
@ -681,4 +387,17 @@ public class Middleware {
service.blockSetIconName(request);
}
public void setupBookmark(CommandEntity.SetupBookmark command) throws Exception {
Block.Bookmark.Fetch.Request request = Block.Bookmark.Fetch.Request
.newBuilder()
.setBlockId(command.getTarget())
.setContextId(command.getContext())
.setUrl(command.getUrl())
.build();
Timber.d("Fetching bookmark with the following request:\n%s", request.toString());
service.blockBookmarkFetch(request);
}
}

View file

@ -23,8 +23,9 @@ class MiddlewareEventChannel(
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS,
Events.Event.Message.ValueCase.BLOCKDELETE,
Events.Event.Message.ValueCase.BLOCKSETLINK,
Events.Event.Message.ValueCase.BLOCKSETFILE,
Events.Event.Message.ValueCase.BLOCKSETFIELDS,
Events.Event.Message.ValueCase.BLOCKSETFILE
Events.Event.Message.ValueCase.BLOCKSETBOOKMARK
)
override fun observeEvents(
@ -126,6 +127,31 @@ class MiddlewareEventChannel(
)
}
}
Events.Event.Message.ValueCase.BLOCKSETBOOKMARK -> {
EventEntity.Command.BookmarkGranularChange(
context = context,
target = event.blockSetBookmark.id,
url = if (event.blockSetBookmark.hasUrl())
event.blockSetBookmark.url.value
else null,
title = if (event.blockSetBookmark.hasTitle())
event.blockSetBookmark.title.value
else
null,
description = if (event.blockSetBookmark.hasDescription())
event.blockSetBookmark.description.value
else
null,
imageHash = if (event.blockSetBookmark.hasImageHash())
event.blockSetBookmark.imageHash.value
else
null,
faviconHash = if (event.blockSetBookmark.hasFaviconHash())
event.blockSetBookmark.faviconHash.value
else
null
)
}
else -> null
}
}

View file

@ -0,0 +1,45 @@
package com.agileburo.anytype.middleware.interactor
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.middleware.toMiddleware
class MiddlewareFactory {
fun create(prototype: BlockEntity.Prototype): Block {
val builder = Block.newBuilder()
return when (prototype) {
is BlockEntity.Prototype.Bookmark -> {
val bookmark = Block.Content.Bookmark.getDefaultInstance()
builder.setBookmark(bookmark).build()
}
is BlockEntity.Prototype.Text -> {
val text = Block.Content.Text.newBuilder().apply {
style = prototype.style.toMiddleware()
}
builder.setText(text).build()
}
is BlockEntity.Prototype.Divider -> {
val divider = Block.Content.Div.newBuilder().apply {
style = Block.Content.Div.Style.Line
}
builder.setDiv(divider).build()
}
is BlockEntity.Prototype.File -> {
val file = Block.Content.File.newBuilder().apply {
state = prototype.state.toMiddleware()
type = prototype.type.toMiddleware()
}
builder.setFile(file).build()
}
is BlockEntity.Prototype.Page -> {
val page = Block.Content.Page.newBuilder().apply {
style = Block.Content.Page.Style.Empty
}
builder.setPage(page).build()
}
}
}
}

View file

@ -0,0 +1,17 @@
package com.agileburo.anytype.middleware.interactor
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.PositionEntity
import com.agileburo.anytype.middleware.toMiddleware
class MiddlewareMapper {
fun toMiddleware(style: BlockEntity.Content.Text.Style): Block.Content.Text.Style {
return style.toMiddleware()
}
fun toMiddleware(position: PositionEntity): Block.Position {
return position.toMiddleware()
}
}

View file

@ -273,4 +273,15 @@ public class DefaultMiddlewareService implements MiddlewareService {
return response;
}
}
@Override
public Block.Bookmark.Fetch.Response blockBookmarkFetch(Block.Bookmark.Fetch.Request request) throws Exception {
byte[] encoded = Lib.blockBookmarkFetch(request.toByteArray());
Block.Bookmark.Fetch.Response response = Block.Bookmark.Fetch.Response.parseFrom(encoded);
if (response.getError() != null && response.getError().getCode() != Block.Bookmark.Fetch.Response.Error.Code.NULL) {
throw new Exception(response.getError().getDescription());
} else {
return response;
}
}
}

View file

@ -6,7 +6,6 @@ import anytype.Commands.Rpc.BlockList;
import anytype.Commands.Rpc.Config;
import anytype.Commands.Rpc.Ipfs.Image;
import anytype.Commands.Rpc.Wallet;
import anytype.Events;
/**
* Service for interacting with the backend.
@ -58,5 +57,7 @@ public interface MiddlewareService {
Block.Set.Icon.Name.Response blockSetIconName(Block.Set.Icon.Name.Request request) throws Exception;
Block.Bookmark.Fetch.Response blockBookmarkFetch(Block.Bookmark.Fetch.Request request) throws Exception;
Block.Upload.Response blockUpload(Block.Upload.Request request) throws Exception;
}

View file

@ -2,6 +2,8 @@ package com.agileburo.anytype
import anytype.Commands.Rpc.Account
import com.agileburo.anytype.middleware.interactor.Middleware
import com.agileburo.anytype.middleware.interactor.MiddlewareFactory
import com.agileburo.anytype.middleware.interactor.MiddlewareMapper
import com.agileburo.anytype.middleware.service.MiddlewareService
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
@ -18,10 +20,13 @@ class MiddlewareTest {
private lateinit var middleware: Middleware
private val mapper = MiddlewareMapper()
private val factory = MiddlewareFactory()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
middleware = Middleware(service)
middleware = Middleware(service, factory, mapper)
}
@Test

View file

@ -66,6 +66,21 @@ class DocumentExternalEventReducer : StateReducer<List<Block>, Event> {
},
target = { block -> block.id == event.id }
)
is Event.Command.BookmarkGranularChange -> state.replace(
replacement = { block ->
val content = block.content<Block.Content.Bookmark>()
block.copy(
content = content.copy(
url = event.url ?: content.url,
title = event.title ?: content.title,
description = event.description ?: content.description,
image = event.image ?: content.image,
favicon = event.favicon ?: content.favicon
)
)
},
target = { block -> block.id == event.target }
)
else -> state.also { Timber.d("Ignoring event: $event") }
}

View file

@ -310,14 +310,20 @@ class PageViewModel(
is Content.Divider -> block.toView(
urlBuilder = urlBuilder
)
is Content.Bookmark -> BlockView.Bookmark(
id = block.id,
url = content.url,
title = content.title,
description = content.description,
imageUrl = content.image?.let { urlBuilder.image(it) },
faviconUrl = content.favicon?.let { urlBuilder.image(it) }
)
is Content.Bookmark -> {
content.url?.let { url ->
BlockView.Bookmark.View(
id = block.id,
url = url,
title = content.title,
description = content.description,
imageUrl = content.image?.let { urlBuilder.image(it) },
faviconUrl = content.favicon?.let { urlBuilder.image(it) }
)
} ?: BlockView.Bookmark.Placeholder(
id = block.id
)
}
else -> null
}
}
@ -772,10 +778,12 @@ class PageViewModel(
)
}
private fun proceedWithCreatingEmptyFileBlock(id: String,
type: Content.File.Type,
state: Content.File.State = Content.File.State.EMPTY,
position: Position = Position.BOTTOM) {
private fun proceedWithCreatingEmptyFileBlock(
id: String,
type: Content.File.Type,
state: Content.File.State = Content.File.State.EMPTY,
position: Position = Position.BOTTOM
) {
createBlock.invoke(
scope = viewModelScope,
params = CreateBlock.Params(
@ -866,6 +874,9 @@ class PageViewModel(
is Content.Link -> {
addNewBlockAtTheEnd()
}
is Content.Bookmark -> {
addNewBlockAtTheEnd()
}
else -> {
Timber.d("Outside-click has been ignored.")
}
@ -889,7 +900,6 @@ class PageViewModel(
}
fun onAddNewPageClicked() {
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
val params = CreateBlock.Params(
@ -907,6 +917,33 @@ class PageViewModel(
}
}
fun onAddBookmarkClicked() {
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
val params = CreateBlock.Params(
context = context,
position = Position.BOTTOM,
target = focusChannel.value,
prototype = Prototype.Bookmark
)
createBlock.invoke(scope = viewModelScope, params = params) { result ->
result.either(
fnL = { Timber.e(it, "Error while creating a bookmark with params: $params") },
fnR = { Timber.d("Bookmark created!") }
)
}
}
fun onBookmarkPlaceholderClicked(target: String) {
dispatch(
command = Command.OpenBookmarkSetter(
context = context,
target = target
)
)
}
fun onTextInputClicked() {
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnTextInputClicked)
}
@ -1028,6 +1065,11 @@ class PageViewModel(
data class OpenGallery(
val mediaType: String
) : Command()
data class OpenBookmarkSetter(
val target: String,
val context: String
) : Command()
}
companion object {

View file

@ -0,0 +1,48 @@
package com.agileburo.anytype.presentation.page.bookmark
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.page.bookmark.SetupBookmark
import com.agileburo.anytype.presentation.page.bookmark.CreateBookmarkViewModel.ViewState
class CreateBookmarkViewModel(
private val setupBookmark: SetupBookmark
) : ViewStateViewModel<ViewState>() {
fun onCreateBookmarkClicked(
context: String,
target: String,
url: String
) {
setupBookmark.invoke(
scope = viewModelScope,
params = SetupBookmark.Params(
context = context,
target = target,
url = url
)
) { result ->
result.either(
fnL = { update(ViewState.Error(it.message ?: toString())) },
fnR = { update(ViewState.Exit) }
)
}
}
sealed class ViewState {
data class Error(val message: String) : ViewState()
object Exit : ViewState()
}
class Factory(
private val setupBookmark: SetupBookmark
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = CreateBookmarkViewModel(
setupBookmark = setupBookmark
) as T
}
}

View file

@ -0,0 +1,216 @@
package com.agileburo.anytype.presentation.page
import MockDataFactory
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.ext.content
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.test.assertEquals
class DocumentExternalEventReducerTest {
private val reducer = DocumentExternalEventReducer()
@Test
fun `should apply bookmark granular changes to the state`() {
// SETUP
val title = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.TITLE
)
)
val bookmark = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Bookmark(
url = null,
description = null,
title = null,
favicon = null,
image = null
)
)
val page = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = listOf(title.id, bookmark.id),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
)
)
val state = listOf(page, title, bookmark)
// TESTING
runBlocking {
val bookmarkUrl = MockDataFactory.randomString()
val expected = listOf(
page, title, bookmark.copy(
content = bookmark.content<Block.Content.Bookmark>().copy(
url = bookmarkUrl
)
)
)
val result = reducer.reduce(
state = state,
event = Event.Command.BookmarkGranularChange(
url = bookmarkUrl,
context = page.id,
target = bookmark.id,
title = null,
description = null,
favicon = null,
image = null
)
)
assertEquals(expected = expected, actual = result)
}
runBlocking {
val bookmarkUrl = MockDataFactory.randomString()
val bookmarkTitle = MockDataFactory.randomString()
val bookmarkDescription = MockDataFactory.randomString()
val expected = listOf(
page, title, bookmark.copy(
content = bookmark.content<Block.Content.Bookmark>().copy(
url = bookmarkUrl,
title = bookmarkTitle,
description = bookmarkDescription
)
)
)
val result = reducer.reduce(
state = state,
event = Event.Command.BookmarkGranularChange(
url = bookmarkUrl,
context = page.id,
target = bookmark.id,
title = bookmarkTitle,
description = bookmarkDescription,
favicon = null,
image = null
)
)
assertEquals(expected = expected, actual = result)
}
runBlocking {
val bookmarkUrl = MockDataFactory.randomString()
val bookmarkTitle = MockDataFactory.randomString()
val bookmarkDescription = MockDataFactory.randomString()
val imageHash = MockDataFactory.randomString()
val faviconHash = MockDataFactory.randomString()
val expected = listOf(
page, title, bookmark.copy(
content = bookmark.content<Block.Content.Bookmark>().copy(
url = bookmarkUrl,
title = bookmarkTitle,
description = bookmarkDescription,
image = imageHash,
favicon = faviconHash
)
)
)
val result = reducer.reduce(
state = state,
event = Event.Command.BookmarkGranularChange(
url = bookmarkUrl,
context = page.id,
target = bookmark.id,
title = bookmarkTitle,
description = bookmarkDescription,
favicon = faviconHash,
image = imageHash
)
)
assertEquals(expected = expected, actual = result)
}
}
@Test
fun `should not apply bookmark granular changes if there is not bookmark block matched by id`() {
val title = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.TITLE
)
)
val bookmark = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Bookmark(
url = null,
description = null,
title = null,
favicon = null,
image = null
)
)
val page = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = listOf(title.id, bookmark.id),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
)
)
val state = listOf(page, title, bookmark)
// TESTING
runBlocking {
val bookmarkUrl = MockDataFactory.randomString()
val expected = state.toList()
val result = reducer.reduce(
state = state,
event = Event.Command.BookmarkGranularChange(
url = bookmarkUrl,
context = page.id,
target = MockDataFactory.randomString(),
title = null,
description = null,
favicon = null,
image = null
)
)
assertEquals(expected = expected, actual = result)
}
}
}

View file

@ -3,8 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.agileburo.anytype.sample">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@ -16,8 +16,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity" />
<activity android:name=".files.LocalFileActivity">
<intent-filter>

View file

@ -4,7 +4,7 @@ import android.app.Application
import com.agileburo.anytype.core_utils.tools.CrashlyticsTree
import timber.log.Timber
class SampleApp : Application(){
class SampleApp : Application() {
override fun onCreate() {