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

Feature/is 142 video block (#264)

This commit is contained in:
Konstantin Ivanov 2020-03-16 22:11:44 +03:00 committed by GitHub
parent b01e5d458b
commit 5a1a2e3bab
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1613 additions and 166 deletions

View file

@ -95,6 +95,7 @@ dependencies {
//Compile time dependencies
kapt applicationDependencies.daggerCompiler
kapt applicationDependencies.glideCompiler
kapt applicationDependencies.permissionDispCompiler
compileOnly applicationDependencies.javaxAnnotation
compileOnly applicationDependencies.javaxInject
@ -113,6 +114,8 @@ dependencies {
implementation applicationDependencies.gson
implementation applicationDependencies.rxRelay
implementation applicationDependencies.tableView
implementation applicationDependencies.permissionDisp
implementation applicationDependencies.pickT
implementation applicationDependencies.viewModel
implementation applicationDependencies.viewModelExtensions

View file

@ -55,6 +55,7 @@ class PageModule {
mergeBlocks: MergeBlocks,
splitBlock: SplitBlock,
createPage: CreatePage,
uploadUrl: UploadUrl,
documentExternalEventReducer: DocumentExternalEventReducer,
urlBuilder: UrlBuilder,
downloadFile: DownloadFile,
@ -75,6 +76,7 @@ class PageModule {
updateLinkMarks = updateLinkMarks,
removeLinkMark = removeLinkMark,
mergeBlocks = mergeBlocks,
uploadUrl = uploadUrl,
splitBlock = splitBlock,
documentEventReducer = documentExternalEventReducer,
urlBuilder = urlBuilder,
@ -163,6 +165,14 @@ class PageModule {
repo = repo
)
@Provides
@PerScreen
fun provideUploadUrl(
repo: BlockRepository
): UploadUrl = UploadUrl(
repo = repo
)
@Provides
@PerScreen
fun provideUpdateLinkMarks(): UpdateLinkMarks = UpdateLinkMarks()

View file

@ -1,11 +1,17 @@
package com.agileburo.anytype.ui.page
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import androidx.activity.addCallback
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
@ -25,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_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
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_HEADER_TWO
@ -32,10 +39,7 @@ import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionC
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TEXT_TEXT
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_TOOL_PAGE
import com.agileburo.anytype.core_utils.common.EventWrapper
import com.agileburo.anytype.core_utils.ext.gone
import com.agileburo.anytype.core_utils.ext.hexColorCode
import com.agileburo.anytype.core_utils.ext.hideSoftInput
import com.agileburo.anytype.core_utils.ext.toast
import com.agileburo.anytype.core_utils.ext.*
import com.agileburo.anytype.di.common.componentManager
import com.agileburo.anytype.domain.block.model.Block.Content.Text
import com.agileburo.anytype.domain.common.Id
@ -48,23 +52,30 @@ import com.agileburo.anytype.ui.base.NavigationFragment
import com.agileburo.anytype.ui.page.modals.PageIconPickerFragment
import com.agileburo.anytype.ui.page.modals.SetLinkFragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.hbisoft.pickit.PickiT
import com.hbisoft.pickit.PickiTCallbacks
import kotlinx.android.synthetic.main.fragment_page.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import permissions.dispatcher.*
import timber.log.Timber
import javax.inject.Inject
const val REQUEST_FILE_CODE = 745
@RuntimePermissions
open class PageFragment : NavigationFragment(R.layout.fragment_page),
OnFragmentInteractionListener {
OnFragmentInteractionListener, PickiTCallbacks {
private val vm by lazy {
ViewModelProviders
.of(this, factory)
.get(PageViewModel::class.java)
}
private lateinit var pickiT: PickiT
private val pageAdapter by lazy {
BlockAdapter(
@ -93,16 +104,87 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
onPageClicked = vm::onPageClicked,
onTextInputClicked = vm::onTextInputClicked,
onDownloadFileClicked = vm::onDownloadFileClicked,
onPageIconClicked = vm::onPageIconClicked
onPageIconClicked = vm::onPageIconClicked,
onAddUrlClick = vm::onAddVideoUrlClicked,
onAddLocalVideoClick = vm::onAddLocalVideoClicked,
strVideoError = getString(R.string.error)
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_FILE_CODE -> {
data?.data?.let {
pickiT.getPath(it, Build.VERSION.SDK_INT)
} ?: run {
toast("Error while getting file")
}
}
else -> toast("Unknown Request Code:$requestCode")
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
fun openGallery(type: String) {
startActivityForResult(getVideoFileIntent(type), REQUEST_FILE_CODE)
}
@OnShowRationale(Manifest.permission.READ_EXTERNAL_STORAGE)
fun showRationaleForRead(request: PermissionRequest) {
showRationaleDialog(R.string.permission_read_rationale, request)
}
@OnPermissionDenied(Manifest.permission.READ_EXTERNAL_STORAGE)
fun onReadDenied() {
toast(getString(R.string.permission_read_denied))
}
@OnNeverAskAgain(Manifest.permission.READ_EXTERNAL_STORAGE)
fun onReadNeverAskAgain() {
toast(getString(R.string.permission_read_never_ask_again))
}
override fun PickiTonProgressUpdate(progress: Int) {
Timber.d("PickiTonProgressUpdate progress:$progress")
}
override fun PickiTonStartListener() {
vm.onChooseVideoFileFromMedia()
Timber.d("PickiTonStartListener")
}
override fun PickiTonCompleteListener(
path: String?,
wasDriveFile: Boolean,
wasUnknownProvider: Boolean,
wasSuccessful: Boolean,
Reason: String?
) {
Timber.d("PickiTonCompleteListener path:$path, wasDriveFile$wasDriveFile, " +
"wasUnknownProvider:$wasUnknownProvider, wasSuccessful:$wasSuccessful, reason:$Reason")
vm.onAddVideoFileClicked(filePath = path)
}
@Inject
lateinit var factory: PageViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm.open(requireArguments().getString(ID_KEY, ID_EMPTY_VALUE))
pickiT = PickiT(requireContext(), this)
setupOnBackPressedDispatcher()
}
@ -271,6 +353,12 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
else -> toast(NOT_IMPLEMENTED_MESSAGE)
}
}
is Option.Media -> {
when (option.type) {
OPTION_MEDIA_VIDEO -> vm.onAddVideoBlockClicked()
else -> toast(NOT_IMPLEMENTED_MESSAGE)
}
}
is Option.Other -> vm.onOptionOtherActionClicked()
}
}
@ -355,6 +443,9 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenGallery -> {
openGalleryWithPermissionCheck(command.mediaType)
}
}
}
}
@ -373,6 +464,7 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
rangeStart = state.range.first
).show(childFragmentManager, null)
}
is PageViewModel.ViewState.Error -> toast(state.message)
}
}
@ -445,6 +537,15 @@ open class PageFragment : NavigationFragment(R.layout.fragment_page),
hideSoftInput()
}
private fun showRationaleDialog(@StringRes messageResId: Int, request: PermissionRequest) {
AlertDialog.Builder(requireContext())
.setPositiveButton(R.string.button_allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.button_deny) { _, _ -> request.cancel() }
.setCancelable(false)
.setMessage(messageResId)
.show()
}
override fun injectDependencies() {
componentManager().pageComponent.get().inject(this)
}

View file

@ -91,6 +91,13 @@ Do the computation of an expensive paragraph of text on a background thread:
<string name="button_unlink">Unlink</string>
<string name="button_link">Link</string>
<string name="permission_read_rationale">Read permission is needed to load file to media block.</string>
<string name="permission_read_denied">Read permission was denied. Please consider granting it in order to load files to media blocks!</string>
<string name="permission_read_never_ask_again">Read permission was denied with never ask again.</string>
<string name="button_allow">Allow</string>
<string name="button_deny">Deny</string>
<string name="error">Error</string>
<string name="page_icon">Page icon</string>
<string name="page_icon_picker_remove_text">Remove</string>

View file

@ -56,6 +56,7 @@ dependencies {
implementation applicationDependencies.glide
implementation applicationDependencies.timber
implementation applicationDependencies.betterLinkMovement
implementation applicationDependencies.exoPlayer
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest

View file

@ -25,6 +25,10 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOL
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TASK
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TITLE
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TOGGLE
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_EMPTY
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_ERROR
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_UPLOAD
import com.agileburo.anytype.core_utils.ext.typeOf
import timber.log.Timber
@ -48,6 +52,9 @@ class BlockAdapter(
private val onFooterClicked: () -> Unit,
private val onPageClicked: (String) -> Unit,
private val onTextInputClicked: () -> Unit,
private val onAddUrlClick: (String, String) -> Unit,
private val onAddLocalVideoClick : (String) -> Unit,
private val strVideoError: String,
private val onPageIconClicked: () -> Unit,
private val onDownloadFileClicked: (String) -> Unit
) : RecyclerView.Adapter<BlockViewHolder>() {
@ -174,6 +181,42 @@ class BlockAdapter(
)
)
}
HOLDER_VIDEO -> {
BlockViewHolder.Video(
view = inflater.inflate(
R.layout.item_block_video,
parent,
false
)
)
}
HOLDER_VIDEO_EMPTY -> {
BlockViewHolder.VideoEmpty(
view = inflater.inflate(
R.layout.item_block_video_empty,
parent,
false
)
)
}
HOLDER_VIDEO_UPLOAD -> {
BlockViewHolder.VideoUpload(
view = inflater.inflate(
R.layout.item_block_video_uploading,
parent,
false
)
)
}
HOLDER_VIDEO_ERROR -> {
BlockViewHolder.VideoError(
view = inflater.inflate(
R.layout.item_block_video_error,
parent,
false
)
)
}
HOLDER_PAGE -> {
BlockViewHolder.Page(
view = inflater.inflate(
@ -387,6 +430,22 @@ class BlockAdapter(
onDownloadFileClicked = onDownloadFileClicked
)
}
is BlockViewHolder.Video -> {
holder.bind(
item = blocks[position] as BlockView.Video
)
}
is BlockViewHolder.VideoError -> {
holder.bind(
msg = strVideoError
)
}
is BlockViewHolder.VideoEmpty -> {
holder.bind(
item = blocks[position] as BlockView.VideoEmpty,
onAddLocalVideoClick = onAddLocalVideoClick
)
}
is BlockViewHolder.Page -> {
holder.bind(
item = blocks[position] as BlockView.Page,

View file

@ -23,6 +23,10 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOL
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TASK
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TITLE
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_TOGGLE
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_EMPTY
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_ERROR
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_UPLOAD
/**
* UI-models for different types of blocks.
@ -275,14 +279,62 @@ sealed class BlockView : ViewType {
*/
data class File(
override val id: String,
val size: Long,
val name: String,
val mime: String,
val size: Long?,
val name: String?,
val mime: String?,
val url: String
) : BlockView() {
override fun getViewType() = HOLDER_FILE
}
/**
* UI-model for blocks containing videos, with state DONE.
* @property id block's id
* @property size a file's size
* @property name a name
* @property size file size (in bytes)
*/
data class Video(
override val id: String,
val size: Long?,
val name: String?,
val mime: String?,
val hash: String?,
val url: String
) : BlockView() {
override fun getViewType() = HOLDER_VIDEO
}
/**
* UI-model for blocks containing videos, with state UPLOADING.
* @property id block's id
*/
data class VideoUpload(
override val id: String
) : BlockView() {
override fun getViewType() = HOLDER_VIDEO_UPLOAD
}
/**
* UI-model for blocks containing videos, with state EMPTY.
* @property id block's id
*/
data class VideoEmpty(
override val id: String
) : BlockView() {
override fun getViewType() = HOLDER_VIDEO_EMPTY
}
/**
* UI-model for blocks containing videos, with state ERROR.
* @property id block's id
*/
data class VideoError(
override val id: String
) : BlockView() {
override fun getViewType() = HOLDER_VIDEO_ERROR
}
/**
* UI-model for blocks containing page links.
* @property id block's id

View file

@ -1,11 +1,13 @@
package com.agileburo.anytype.core_ui.features.page
import android.graphics.Color
import android.net.Uri
import android.graphics.drawable.Drawable
import android.text.Editable
import android.view.View
import android.widget.TextView.BufferType
import androidx.recyclerview.widget.RecyclerView
import com.agileburo.anytype.core_ui.BuildConfig
import com.agileburo.anytype.core_ui.R
import com.agileburo.anytype.core_ui.common.*
import com.agileburo.anytype.core_ui.extensions.color
@ -26,6 +28,10 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.item_block_bookmark.view.*
import kotlinx.android.synthetic.main.item_block_bulleted.view.*
import kotlinx.android.synthetic.main.item_block_checkbox.view.*
@ -43,6 +49,8 @@ import kotlinx.android.synthetic.main.item_block_task.view.*
import kotlinx.android.synthetic.main.item_block_text.view.*
import kotlinx.android.synthetic.main.item_block_title.view.*
import kotlinx.android.synthetic.main.item_block_toggle.view.*
import kotlinx.android.synthetic.main.item_block_video.view.*
import kotlinx.android.synthetic.main.item_block_video_error.view.*
import timber.log.Timber
import android.text.format.Formatter as FileSizeFormatter
@ -617,8 +625,10 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
onDownloadFileClicked: (String) -> Unit
) {
name.text = item.name
size.text = FileSizeFormatter.formatFileSize(itemView.context, item.size)
when (MimeTypes.category(item.mime)) {
item.size?.let {
size.text = FileSizeFormatter.formatFileSize(itemView.context, it)
}
when (item.mime?.let { MimeTypes.category(it) }) {
MimeTypes.Category.PDF -> icon.setImageResource(R.drawable.ic_mime_pdf)
MimeTypes.Category.IMAGE -> icon.setImageResource(R.drawable.ic_mime_image)
MimeTypes.Category.AUDIO -> icon.setImageResource(R.drawable.ic_mime_music)
@ -628,11 +638,57 @@ 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) }
}
}
class Video(view: View) : BlockViewHolder(view) {
private val icon = itemView.fileIcon
private val size = itemView.fileSize
private val name = itemView.filename
fun bind(item: BlockView.Video) {
initPlayer(item.url)
}
private fun initPlayer(path: String) {
itemView.playerView.visibility = View.VISIBLE
val player = SimpleExoPlayer.Builder(itemView.context).build()
val source = DefaultDataSourceFactory(
itemView.context,
Util.getUserAgent(itemView.context, BuildConfig.LIBRARY_PACKAGE_NAME)
)
val mediaSource =
ProgressiveMediaSource.Factory(source).createMediaSource(Uri.parse(path))
player.playWhenReady = false
player.seekTo(0)
player.prepare(mediaSource, false, false)
itemView.playerView.player = player
}
}
class VideoUpload(view: View) : BlockViewHolder(view)
class VideoError(view: View) : BlockViewHolder(view) {
fun bind(msg: String) {
itemView.tvError.text = msg
}
}
class VideoEmpty(view: View) : BlockViewHolder(view) {
fun bind(item: BlockView.VideoEmpty, onAddLocalVideoClick: (String) -> Unit) {
itemView.setOnClickListener {
onAddLocalVideoClick(item.id)
}
}
}
class Page(view: View) : BlockViewHolder(view) {
private val untitled = itemView.resources.getString(R.string.untitled)
@ -799,6 +855,10 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
const val HOLDER_DIVIDER = 16
const val HOLDER_HIGHLIGHT = 17
const val HOLDER_FOOTER = 18
const val HOLDER_VIDEO = 19
const val HOLDER_VIDEO_UPLOAD = 20
const val HOLDER_VIDEO_EMPTY = 21
const val HOLDER_VIDEO_ERROR = 22
const val FOCUS_TIMEOUT_MILLIS = 16L
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
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"/>
</vector>

View file

@ -0,0 +1,9 @@
<?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="8dp" />
</shape>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/default_page_item_padding_start"
android:layout_marginTop="6dp"
android:layout_marginEnd="@dimen/default_page_item_padding_end"
android:layout_marginBottom="6dp"
android:paddingBottom="1dp">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="250dp"
android:visibility="gone" />
</FrameLayout>

View file

@ -0,0 +1,54 @@
<?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"
android:layout_marginTop="6dp"
android:layout_marginEnd="@dimen/default_page_item_padding_end"
android:layout_marginBottom="6dp"
android:background="@drawable/rectangle_block_media_background"
android:paddingTop="1dp"
android:paddingBottom="1dp">
<ImageView
android:id="@+id/icVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:contentDescription="@string/content_description_file_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_video" />
<TextView
android:id="@+id/editUrl"
style="@style/BlockVideoStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="24dp"
android:textColor="#ACA996"
android:text="@string/hint_upload"
android:inputType="none"
app:layout_constraintBottom_toBottomOf="@+id/icVideo"
app:layout_constraintEnd_toStartOf="@+id/icMore"
app:layout_constraintStart_toEndOf="@+id/icVideo"
app:layout_constraintTop_toTopOf="@+id/icVideo" />
<ImageView
android:id="@+id/icMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_block_more" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,51 @@
<?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"
android:layout_marginTop="6dp"
android:layout_marginEnd="@dimen/default_page_item_padding_end"
android:layout_marginBottom="6dp"
android:background="@drawable/rectangle_block_media_background"
android:paddingTop="1dp"
android:paddingBottom="1dp">
<ImageView
android:id="@+id/icVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:contentDescription="@string/content_description_file_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_video" />
<TextView
android:id="@+id/tvError"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="24dp"
android:textColor="@android:color/holo_red_dark"
app:layout_constraintBottom_toBottomOf="@+id/icVideo"
app:layout_constraintEnd_toStartOf="@+id/icMore"
app:layout_constraintStart_toEndOf="@+id/icVideo"
app:layout_constraintTop_toTopOf="@+id/icVideo" />
<ImageView
android:id="@+id/icMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_block_more" />
</androidx.constraintlayout.widget.ConstraintLayout>

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"
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"
android:layout_marginTop="6dp"
android:layout_marginEnd="@dimen/default_page_item_padding_end"
android:layout_marginBottom="6dp"
android:background="@drawable/rectangle_block_media_background"
android:paddingTop="1dp"
android:paddingBottom="1dp">
<ImageView
android:id="@+id/icVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:contentDescription="@string/content_description_file_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_video" />
<TextView
android:id="@+id/editUrl"
style="@style/BlockVideoStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="Loading, please wait"
android:textColor="#ACA996"
app:layout_constraintBottom_toBottomOf="@+id/icVideo"
app:layout_constraintStart_toEndOf="@+id/icVideo"
app:layout_constraintTop_toTopOf="@+id/icVideo" />
<ImageView
android:id="@+id/icMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_block_more" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@+id/editUrl"
app:layout_constraintStart_toEndOf="@+id/editUrl"
app:layout_constraintTop_toTopOf="@+id/editUrl" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -119,6 +119,8 @@
<string name="hint_header_two">Header 2</string>
<string name="hint_header_three">Header 3</string>
<string name="hint_checkbox">Checkbox</string>
<string name="hint_video">Enter video URL</string>
<string name="hint_upload">Upload a video</string>
<string name="hint_bullet">Bulleted list item</string>
<string name="hint_numbered_list">Numbered list item</string>

View file

@ -177,6 +177,15 @@
<item name="android:layout_marginBottom">3dp</item>
</style>
<style name="BlockVideoStyle">
<item name="android:textSize">15sp</item>
<item name="android:textColor">@color/black</item>
<item name="android:inputType">textUri</item>
<item name="android:textColorHint">#ACA996</item>
<item name="android:background">@null</item>
<item name="android:fontFamily">@font/graphik_regular</item>
</style>
<style name="BlockContactContentStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>

View file

@ -848,7 +848,10 @@ class BlockAdapterTest {
onPageClicked = {},
onTextInputClicked = {},
onDownloadFileClicked = {},
onPageIconClicked = {}
onPageIconClicked = {},
onAddLocalVideoClick = {},
onAddUrlClick = { _, _ -> },
strVideoError = "Error"
)
}
}

View file

@ -0,0 +1,41 @@
package com.agileburo.anytype.core_ui.features.page
import org.junit.Assert.assertEquals
import org.junit.Test
class BlockViewTest {
val ID = "123"
@Test
fun `should return video block with view type Empty`() {
val block = BlockView.VideoEmpty(id = ID)
assertEquals(BlockViewHolder.HOLDER_VIDEO_EMPTY, block.getViewType())
}
@Test
fun `should return video block with view type Error`() {
val block = BlockView.VideoError(id = ID)
assertEquals(BlockViewHolder.HOLDER_VIDEO_ERROR, block.getViewType())
}
@Test
fun `should return video block with view type Done`() {
val block = BlockView.Video(id = ID, hash = "", url = "", size = 0L, mime = "", name = "")
assertEquals(BlockViewHolder.HOLDER_VIDEO, block.getViewType())
}
@Test
fun `should return video block with view type Uploading`() {
val block = BlockView.VideoUpload(id = ID)
assertEquals(BlockViewHolder.HOLDER_VIDEO_UPLOAD, block.getViewType())
}
}

View file

@ -1,9 +1,11 @@
package com.agileburo.anytype.core_utils.ext
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Rect
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.text.Annotation
import android.text.Editable
@ -49,6 +51,8 @@ fun Throwable.timber() = Timber.e("Get error : ${this.message}")
const val DATE_FORMAT_MMMdYYYY = "MMM d, yyyy"
const val KEY_ROUNDED = "key"
const val VALUE_ROUNDED = "rounded"
const val MIME_VIDEO_ALL = "video/*"
const val MIME_IMAGE_ALL = "image/*"
fun Long.formatToDateString(pattern: String, locale: Locale): String {
val formatter = SimpleDateFormat(pattern, locale)
@ -103,4 +107,19 @@ fun Editable.removeRoundedSpans(): Editable {
if (span.key == KEY_ROUNDED && span.value == VALUE_ROUNDED) removeSpan(span)
}
return this
}
fun getVideoFileIntent(mediaType: String): Intent {
val intent =
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
} else {
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI)
}
return intent.apply {
type = mediaType
action = Intent.ACTION_GET_CONTENT
putExtra("return-data", true)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}

View file

@ -91,10 +91,9 @@ fun BlockEntity.Content.File.toDomain(): Block.Content.File {
hash = hash,
name = name,
mime = mime,
added = added,
size = size,
type = type.toDomain(),
state = state.toDomain()
type = type?.toDomain(),
state = state?.toDomain()
)
}
@ -222,10 +221,9 @@ fun Block.Content.File.toEntity(): BlockEntity.Content.File {
hash = hash,
name = name,
mime = mime,
added = added,
size = size,
type = type.toEntity(),
state = state.toEntity()
type = type?.toEntity(),
state = state?.toEntity()
)
}
@ -387,6 +385,13 @@ fun Command.SetIconName.toEntity() = CommandEntity.SetIconName(
name = name
)
fun Command.UploadVideoBlockUrl.toEntity(): CommandEntity.UploadBlock = CommandEntity.UploadBlock(
contextId = contextId,
blockId = blockId,
url = url,
filePath = filePath
)
fun Position.toEntity(): PositionEntity {
return PositionEntity.valueOf(name)
}
@ -455,6 +460,18 @@ fun EventEntity.toDomain(): Event {
fields = Block.Fields(fields.map)
)
}
is EventEntity.Command.UpdateBlockFile -> {
Event.Command.UpdateFileBlock(
context = context,
id = id,
type = type?.toDomain(),
state = state?.toDomain(),
size = size,
mime = mime,
hash = hash,
name = name
)
}
}
}
@ -470,4 +487,10 @@ fun Block.Prototype.toEntity(): BlockEntity.Prototype = when (this) {
)
}
Block.Prototype.Divider -> BlockEntity.Prototype.Divider
is Block.Prototype.File -> {
BlockEntity.Prototype.File(
type = BlockEntity.Content.File.Type.valueOf(this.type.name),
state = BlockEntity.Content.File.State.valueOf(this.state.name)
)
}
}

View file

@ -73,13 +73,12 @@ data class BlockEntity(
}
data class File(
val hash: String,
val name: String,
val type: Type,
val mime: String,
val size: Long,
val added: Long,
val state: State
val hash: String? = null,
val name: String? = null,
val type: Type? = null,
val mime: String? = null,
val size: Long? = null,
val state: State? = null
) : Content() {
enum class Type { NONE, FILE, IMAGE, VIDEO }
enum class State { EMPTY, UPLOADING, DONE, ERROR }
@ -106,5 +105,10 @@ data class BlockEntity(
) : Prototype()
object Divider : Prototype()
data class File(
val state: Content.File.State,
val type: Content.File.Type
) : Prototype()
}
}

View file

@ -36,6 +36,13 @@ class CommandEntity {
val isChecked: Boolean
)
class UploadBlock(
val contextId: String,
val blockId: String,
val url: String,
val filePath: String
)
class Create(
val context: String,
val target: String,

View file

@ -56,5 +56,16 @@ sealed class EventEntity {
val target: String,
val fields: BlockEntity.Fields
) : Command()
data class UpdateBlockFile(
override val context: String,
val id: String,
val type: BlockEntity.Content.File.Type? = null,
val state: BlockEntity.Content.File.State? = null,
val hash: String? = null,
val name: String? = null,
val size: Long? = null,
val mime: String? = null
) : Command()
}
}

View file

@ -70,4 +70,8 @@ class BlockDataRepository(
override suspend fun setIconName(command: Command.SetIconName) =
factory.remote.setIconName(command.toEntity())
override suspend fun uploadUrl(command: Command.UploadVideoBlockUrl) {
factory.remote.uploadUrl(command.toEntity())
}
}

View file

@ -11,6 +11,7 @@ interface BlockDataStore {
suspend fun updateTextColor(command: CommandEntity.UpdateTextColor)
suspend fun updateBackroundColor(command: CommandEntity.UpdateBackgroundColor)
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
suspend fun uploadUrl(command: CommandEntity.UploadBlock)
suspend fun dnd(command: CommandEntity.Dnd)
suspend fun duplicate(command: CommandEntity.Duplicate): Id
suspend fun merge(command: CommandEntity.Merge)

View file

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

View file

@ -43,6 +43,10 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
remote.updateCheckbox(command)
}
override suspend fun uploadUrl(command: CommandEntity.UploadBlock) {
remote.uploadUrl(command)
}
override suspend fun create(command: CommandEntity.Create): String = remote.create(command)
override suspend fun dnd(command: CommandEntity.Dnd) {

View file

@ -23,6 +23,7 @@ ext {
navigation_version = '2.2.0-rc01'
// Third party libraries
exoplayer_version = '2.11.3'
glide_version = '4.9.0'
dagger_version = '2.11'
javaxAnnotations_version = '1.0'
@ -36,6 +37,8 @@ ext {
better_link_method_version = '2.2.0'
table_view_version = '0.8.8'
rxbinding_version = '3.0.0'
permission_disp_version = '4.6.0'
pickt_version = "0.1.9"
// Unit Testing
robolectric_version = '4.3.1'
@ -107,6 +110,10 @@ ext {
moshiKotlin: "com.squareup.moshi:moshi-kotlin:$moshi_version",
tableView: "com.evrencoskun.library:tableview:$table_view_version",
rxBinding: "com.jakewharton.rxbinding3:rxbinding:$rxbinding_version",
exoPlayer: "com.google.android.exoplayer:exoplayer:$exoplayer_version",
permissionDisp: "org.permissionsdispatcher:permissionsdispatcher:$permission_disp_version",
permissionDispCompiler: "org.permissionsdispatcher:permissionsdispatcher-processor:$permission_disp_version",
pickT: "com.github.HBiSoft:PickiT:$pickt_version",
crashlytics: "com.crashlytics.sdk.android:crashlytics:$crashlytics_version",
firebaseCore: "com.google.firebase:firebase-core:$firebase_core_version"

View file

@ -0,0 +1,31 @@
package com.agileburo.anytype.domain.block.interactor
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
class UploadUrl(private val repo: BlockRepository) : BaseUseCase<Unit, UploadUrl.Params>() {
override suspend fun run(params: Params): Either<Throwable, Unit> = try {
repo.uploadUrl(
command = Command.UploadVideoBlockUrl(
contextId = params.contextId,
blockId = params.blockId,
url = params.url,
filePath = params.filePath
)
).let {
Either.Right(it)
}
} catch (t: Throwable) {
Either.Left(t)
}
data class Params(
val contextId: String,
val blockId: String,
val url: String,
val filePath: String
)
}

View file

@ -46,6 +46,7 @@ data class Block(
fun asLink() = this as Link
fun asDashboard() = this as Dashboard
fun asDivider() = this as Divider
fun asFile() = this as File
/**
* Textual block.
@ -159,13 +160,12 @@ data class Block(
* @property state file state
*/
data class File(
val hash: String,
val name: String,
val mime: String,
val size: Long,
val added: Long,
val type: Type,
val state: State
val hash: String? = null,
val name: String? = null,
val mime: String? = null,
val size: Long? = null,
val type: Type? = null,
val state: State? = null
) : Content() {
enum class Type { NONE, FILE, IMAGE, VIDEO }
enum class State { EMPTY, UPLOADING, DONE, ERROR }
@ -206,5 +206,10 @@ data class Block(
) : Prototype()
object Divider : Prototype()
data class File(
val type: Content.File.Type,
val state: Content.File.State
) : Prototype()
}
}

View file

@ -128,6 +128,20 @@ sealed class Command {
val index: Int
)
/**
* Command for updating video block url
* @property contextId context id
* @property blockId id of the video block
* @property url new valid url
* @property filePath file uri
*/
data class UploadVideoBlockUrl(
val contextId: Id,
val blockId: Id,
val url: String,
val filePath: String
)
data class SetIconName(
val context: Id,
val target: Id,

View file

@ -35,5 +35,10 @@ interface BlockRepository {
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
/**
* Upload url for video block.
*/
suspend fun uploadUrl(command: Command.UploadVideoBlockUrl)
suspend fun setIconName(command: Command.SetIconName)
}

View file

@ -86,5 +86,19 @@ sealed class Event {
val target: Id,
val fields: Block.Fields
) : Command()
/**
* Command to update file block content
*/
data class UpdateFileBlock(
override val context: String,
val id: Id,
val state: Block.Content.File.State? = null,
val type: Block.Content.File.Type? = null,
val name: String? = null,
val hash: String? = null,
val mime: String? = null,
val size: Long? = null
) : Command()
}
}

View file

@ -12,16 +12,17 @@ class UrlBuilder(val config: Config) {
/**
* Builds image url for given [hash]
*/
fun image(hash: String): Url {
return config.gateway + IMAGE_PATH + hash
}
fun image(hash: String?): Url = config.gateway + IMAGE_PATH + hash
/**
* Builds file url for given [hash]
*/
fun file(hash: String): Url {
return config.gateway + FILE_PATH + hash
}
fun file(hash: String?): Url = config.gateway + FILE_PATH + hash
/**
* Builds video url for given [hash]
*/
fun video(hash: String?): Url = config.gateway + FILE_PATH + hash
companion object {
const val IMAGE_PATH = "/image/"

View file

@ -0,0 +1,99 @@
package com.agileburo.anytype.domain.misc
import com.agileburo.anytype.domain.config.Config
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class UrlBuilderTest {
private lateinit var config: Config
private lateinit var urlBuilder: UrlBuilder
@Before
fun setup() {
config = Config(home = "67889", gateway = "https://anytype.io")
urlBuilder = UrlBuilder(config)
}
@Test
fun `should return image url`() {
val hash = "image001"
val expected = config.gateway + UrlBuilder.IMAGE_PATH + hash
val actual = urlBuilder.image(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url with null at the end when image hash is null`() {
val hash = null
val expected = config.gateway + UrlBuilder.IMAGE_PATH + null
val actual = urlBuilder.image(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url without hash when image hash is empty`() {
val hash = ""
val expected = config.gateway + UrlBuilder.IMAGE_PATH
val actual = urlBuilder.image(hash)
assertEquals(expected, actual)
}
@Test
fun `should return file url`() {
val hash = "file001"
val expected = config.gateway + UrlBuilder.FILE_PATH + hash
val actual = urlBuilder.file(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url with null at the end when file hash is null`() {
val hash = null
val expected = config.gateway + UrlBuilder.FILE_PATH + null
val actual = urlBuilder.file(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url without hash when file hash is empty`() {
val hash = ""
val expected = config.gateway + UrlBuilder.FILE_PATH
val actual = urlBuilder.file(hash)
assertEquals(expected, actual)
}
@Test
fun `should return video url`() {
val hash = "video001"
val expected = config.gateway + UrlBuilder.FILE_PATH + hash
val actual = urlBuilder.video(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url with null at the end when video hash is null`() {
val hash = null
val expected = config.gateway + UrlBuilder.FILE_PATH + null
val actual = urlBuilder.video(hash)
assertEquals(expected, actual)
}
@Test
fun `should return url without hash when video hash is empty`() {
val hash = ""
val expected = config.gateway + UrlBuilder.FILE_PATH
val actual = urlBuilder.video(hash)
assertEquals(expected, actual)
}
}

View file

@ -224,24 +224,27 @@ fun Block.file(): BlockEntity.Content.File = BlockEntity.Content.File(
hash = file.hash,
name = file.name,
size = file.size,
added = file.addedAt,
mime = file.mime,
type = when (file.type) {
Block.Content.File.Type.File -> BlockEntity.Content.File.Type.FILE
Block.Content.File.Type.Image -> BlockEntity.Content.File.Type.IMAGE
Block.Content.File.Type.Video -> BlockEntity.Content.File.Type.VIDEO
Block.Content.File.Type.None -> BlockEntity.Content.File.Type.NONE
else -> throw IllegalStateException("Unexpected file type: $file.type")
},
state = when (file.state) {
Block.Content.File.State.Done -> BlockEntity.Content.File.State.DONE
Block.Content.File.State.Empty -> BlockEntity.Content.File.State.EMPTY
Block.Content.File.State.Uploading -> BlockEntity.Content.File.State.UPLOADING
Block.Content.File.State.Error -> BlockEntity.Content.File.State.ERROR
else -> throw IllegalStateException("Unexpected file state: ${file.state}")
}
type = file.type.entity(),
state = file.state.entity()
)
fun Block.Content.File.Type.entity(): BlockEntity.Content.File.Type = when (this) {
Block.Content.File.Type.File -> BlockEntity.Content.File.Type.FILE
Block.Content.File.Type.Image -> BlockEntity.Content.File.Type.IMAGE
Block.Content.File.Type.Video -> BlockEntity.Content.File.Type.VIDEO
Block.Content.File.Type.None -> BlockEntity.Content.File.Type.NONE
else -> throw IllegalStateException("Unexpected file type: $this")
}
fun Block.Content.File.State.entity(): BlockEntity.Content.File.State = when (this) {
Block.Content.File.State.Done -> BlockEntity.Content.File.State.DONE
Block.Content.File.State.Empty -> BlockEntity.Content.File.State.EMPTY
Block.Content.File.State.Uploading -> BlockEntity.Content.File.State.UPLOADING
Block.Content.File.State.Error -> BlockEntity.Content.File.State.ERROR
else -> throw IllegalStateException("Unexpected file state: $this")
}
fun Block.bookmark(): BlockEntity.Content.Bookmark = BlockEntity.Content.Bookmark(
url = bookmark.url,
description = bookmark.description.ifEmpty { null },

View file

@ -39,6 +39,10 @@ class BlockMiddleware(
)
}
override suspend fun uploadUrl(command: CommandEntity.UploadBlock) {
middleware.uploadMediaBlockContent(command)
}
override suspend fun updateTextStyle(command: CommandEntity.UpdateStyle) {
middleware.updateTextStyle(command)
}

View file

@ -296,15 +296,162 @@ public class Middleware {
service.blockSetTextBackgroundColor(request);
}
public String createBlock(
String contextId,
String targetId,
PositionEntity position,
BlockEntity.Prototype prototype
) throws Exception {
public void uploadMediaBlockContent(CommandEntity.UploadBlock command) throws Exception {
Block.Upload.Request request = Block.Upload.Request
.newBuilder()
.setFilePath(command.getFilePath())
.setUrl(command.getUrl())
.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;
@ -325,120 +472,97 @@ public class Middleware {
positionModel = Models.Block.Position.Inner;
break;
}
return positionModel;
}
Models.Block.Content.Text textBlockModel = null;
Models.Block.Content.Page pageBlockModel = null;
Models.Block.Content.Div dividerBlockModel = null;
private Models.Block createBlock(Models.Block.Content.Text textBlockModel) {
return Models.Block
.newBuilder()
.setText(textBlockModel)
.build();
}
if (prototype instanceof BlockEntity.Prototype.Text) {
private Models.Block createBlock(Models.Block.Content.Page pageBlockModel) {
return Models.Block
.newBuilder()
.setPage(pageBlockModel)
.build();
}
BlockEntity.Content.Text.Style style = ((BlockEntity.Prototype.Text) prototype).getStyle();
private Models.Block createBlock(Models.Block.Content.Div dividerBlockModel) {
return Models.Block
.newBuilder()
.setDiv(dividerBlockModel)
.build();
}
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;
}
} else if (prototype instanceof BlockEntity.Prototype.Page) {
pageBlockModel = Models.Block.Content.Page
.newBuilder()
.setStyle(Models.Block.Content.Page.Style.Empty)
.build();
} else if (prototype instanceof BlockEntity.Prototype.Divider) {
dividerBlockModel = Models.Block.Content.Div
.newBuilder()
.setStyle(Models.Block.Content.Div.Style.Line)
.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 = Models.Block
.newBuilder()
.setText(textBlockModel)
.build();
blockModel = createBlock(textBlockModel);
} else if (pageBlockModel != null) {
blockModel = Models.Block
.newBuilder()
.setPage(pageBlockModel)
.build();
blockModel = createBlock(pageBlockModel);
} else if (dividerBlockModel != null) {
blockModel = Models.Block
.newBuilder()
.setDiv(dividerBlockModel)
.build();
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,
PositionEntity position,
BlockEntity.Prototype prototype
) throws Exception {
Models.Block.Position positionModel = createPosition(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);
Block.Create.Request request = Block.Create.Request
.newBuilder()
.setContextId(contextId)

View file

@ -23,7 +23,8 @@ class MiddlewareEventChannel(
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS,
Events.Event.Message.ValueCase.BLOCKDELETE,
Events.Event.Message.ValueCase.BLOCKSETLINK,
Events.Event.Message.ValueCase.BLOCKSETFIELDS
Events.Event.Message.ValueCase.BLOCKSETFIELDS,
Events.Event.Message.ValueCase.BLOCKSETFILE
)
override fun observeEvents(
@ -111,6 +112,20 @@ class MiddlewareEventChannel(
fields = event.blockSetFields.fields.fields()
)
}
Events.Event.Message.ValueCase.BLOCKSETFILE -> {
with(event.blockSetFile) {
EventEntity.Command.UpdateBlockFile(
context = context,
id = id,
state = if (hasState()) state.value.entity() else null,
type = if (hasType()) type.value.entity() else null,
name = if (hasName()) name.value else null,
hash = if (hasHash()) hash.value else null,
mime = if (hasMime()) mime.value else null,
size = if (hasSize()) size.value else null
)
}
}
else -> null
}
}

View file

@ -252,6 +252,17 @@ public class DefaultMiddlewareService implements MiddlewareService {
}
}
@Override
public Block.Upload.Response blockUpload(Block.Upload.Request request) throws Exception {
byte[] encoded = Lib.blockUpload(request.toByteArray());
Block.Upload.Response response = Block.Upload.Response.parseFrom(encoded);
if (response.getError() != null && response.getError().getCode() != Block.Upload.Response.Error.Code.NULL) {
throw new Exception(response.getError().getDescription());
} else {
return response;
}
}
@Override
public Block.Set.Icon.Name.Response blockSetIconName(Block.Set.Icon.Name.Request request) throws Exception {
byte[] encoded = Lib.blockSetIconName(request.toByteArray());

View file

@ -6,6 +6,7 @@ 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.
@ -56,4 +57,6 @@ public interface MiddlewareService {
BlockList.Duplicate.Response blockListDuplicate(BlockList.Duplicate.Request request) throws Exception;
Block.Set.Icon.Name.Response blockSetIconName(Block.Set.Icon.Name.Request request) throws Exception;
Block.Upload.Response blockUpload(Block.Upload.Request request) throws Exception;
}

View file

@ -2,7 +2,9 @@ package com.agileburo.anytype
import anytype.Events.Event
import anytype.Events.Event.Message
import anytype.model.Models
import com.agileburo.anytype.common.MockDataFactory
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.interactor.MiddlewareEventChannel
@ -150,4 +152,109 @@ class MiddlewareEventChannelTest {
}
}
}
@Test
fun `should return UpdateBlockFile event`() {
val hash = "785687346534hfjdbsjfbds"
val name = "video1.mp4"
val mime = "video/*"
val size = 999111L
val state = Models.Block.Content.File.State.Done
val type = Models.Block.Content.File.Type.Video
val context = MockDataFactory.randomUuid()
val id = MockDataFactory.randomUuid()
val msg = Message
.newBuilder()
.blockSetFileBuilder
.setId(id)
.setHash(Event.Block.Set.File.Hash.newBuilder().setValue(hash).build())
.setMime(Event.Block.Set.File.Mime.newBuilder().setValue(mime).build())
.setSize(Event.Block.Set.File.Size.newBuilder().setValue(size).build())
.setType(Event.Block.Set.File.Type.newBuilder().setValue(type).build())
.setState(Event.Block.Set.File.State.newBuilder().setValue(state).build())
.setName(Event.Block.Set.File.Name.newBuilder().setValue(name).build())
.build()
val message = Message
.newBuilder()
.setBlockSetFile(msg)
val event = Event
.newBuilder()
.setContextId(context)
.addMessages(message)
.build()
proxy.stub {
on { flow() } doReturn flowOf(event)
}
val expected = listOf(
EventEntity.Command.UpdateBlockFile(
context = context,
id = id,
hash = hash,
mime = mime,
size = size,
type = BlockEntity.Content.File.Type.VIDEO,
state = BlockEntity.Content.File.State.DONE,
name = name
)
)
runBlocking {
channel.observeEvents(context = context).collect { events ->
assertEquals(
expected = expected,
actual = events
)
}
}
}
@Test
fun `should return UpdateBlockFile event with nullable values`() {
val context = MockDataFactory.randomUuid()
val id = MockDataFactory.randomUuid()
val msg = Message
.newBuilder()
.blockSetFileBuilder
.setId(id)
.build()
val message = Message
.newBuilder()
.setBlockSetFile(msg)
val event = Event
.newBuilder()
.setContextId(context)
.addMessages(message)
.build()
proxy.stub {
on { flow() } doReturn flowOf(event)
}
val expected = listOf(
EventEntity.Command.UpdateBlockFile(
context = context,
id = id
)
)
runBlocking {
channel.observeEvents(context = context).collect { events ->
assertEquals(
expected = expected,
actual = events
)
}
}
}
}

View file

@ -111,6 +111,12 @@ fun Block.toView(
mime = content.mime,
url = urlBuilder.file(content.hash)
)
Block.Content.File.Type.VIDEO -> content.toVideoView(
id = id,
urlBuilder = urlBuilder
)
Block.Content.File.Type.NONE ->
throw UnsupportedOperationException("File block type None, not implemented")
else -> TODO()
}
}
@ -130,6 +136,22 @@ fun Block.toView(
else -> TODO()
}
fun Block.Content.File.toVideoView(id: String, urlBuilder: UrlBuilder): BlockView =
when (this.state) {
Block.Content.File.State.EMPTY -> BlockView.VideoEmpty(id = id)
Block.Content.File.State.UPLOADING -> BlockView.VideoUpload(id = id)
Block.Content.File.State.DONE -> BlockView.Video(
id = id,
size = size,
name = name,
mime = mime,
hash = hash,
url = urlBuilder.video(hash)
)
Block.Content.File.State.ERROR -> BlockView.VideoError(id = id)
null -> throw NotImplementedError("File block state, should not be null")
}
private fun mapMarks(content: Block.Content.Text): List<Markup.Mark> =
content.marks.mapNotNull { mark ->
when (mark.type) {

View file

@ -49,6 +49,24 @@ class DocumentExternalEventReducer : StateReducer<List<Block>, Event> {
replacement = { block -> block.copy(fields = event.fields) },
target = { block -> block.id == event.target }
)
is Event.Command.UpdateFileBlock -> state.replace(
replacement = { block ->
val content = block.content<Block.Content.File>()
block.copy(
content = content.copy(
hash = event.hash ?: content.hash,
name = event.name ?: content.name,
mime = event.mime ?: content.mime,
size = event.size ?: content.size,
type = event.type ?: content.type,
state = event.state ?: content.state
)
)
},
target = { block -> block.id == event.id }
)
else -> state.also { Timber.d("Ignoring event: $event") }
}
}

View file

@ -7,6 +7,7 @@ import com.agileburo.anytype.core_ui.common.Markup
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.core_ui.state.ControlPanelState
import com.agileburo.anytype.core_utils.common.EventWrapper
import com.agileburo.anytype.core_utils.ext.MIME_VIDEO_ALL
import com.agileburo.anytype.core_utils.ext.replace
import com.agileburo.anytype.core_utils.ext.withLatestFrom
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
@ -37,6 +38,8 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
const val EMPTY_PATH = ""
class PageViewModel(
private val openPage: OpenPage,
private val closePage: ClosePage,
@ -55,6 +58,7 @@ class PageViewModel(
private val mergeBlocks: MergeBlocks,
private val splitBlock: SplitBlock,
private val downloadFile: DownloadFile,
private val uploadUrl: UploadUrl,
private val documentExternalEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder,
private val emojifier: Emojifier
@ -94,6 +98,11 @@ class PageViewModel(
private val _focus: MutableLiveData<Id> = MutableLiveData()
val focus: LiveData<Id> = _focus
/**
* Open gallery and search media files for block with that id
*/
private var mediaBlockId = ""
override val navigation = MutableLiveData<EventWrapper<AppNavigation.Command>>()
override val commands = MutableLiveData<EventWrapper<Command>>()
@ -737,6 +746,52 @@ class PageViewModel(
)
}
fun onAddVideoBlockClicked() {
proceedWithCreatingEmptyFileBlock(
id = focusChannel.value,
type = Content.File.Type.VIDEO
)
}
fun onAddLocalVideoClicked(blockId: String) {
mediaBlockId = blockId
dispatch(Command.OpenGallery(mediaType = MIME_VIDEO_ALL))
}
fun onAddImageBlockClicked() {
proceedWithCreatingEmptyFileBlock(
id = focusChannel.value,
type = Content.File.Type.IMAGE
)
}
fun onAddFileBlockClicked() {
proceedWithCreatingEmptyFileBlock(
id = focusChannel.value,
type = Content.File.Type.FILE
)
}
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(
context = context,
target = id,
position = position,
prototype = Prototype.File(type = type, state = state)
)
) { result ->
result.either(
fnL = { Timber.e(it, "Error while creating a block") },
fnR = { id -> updateFocus(id) }
)
}
}
fun onCheckboxClicked(id: String) {
val target = blocks.first { it.id == id }
@ -868,6 +923,58 @@ class PageViewModel(
}
}
fun onAddVideoUrlClicked(blockId: String, url: String) {
//Todo add url validation
uploadUrl.invoke(
scope = viewModelScope,
params = UploadUrl.Params(
contextId = context,
blockId = blockId,
url = url,
filePath = EMPTY_PATH
)
) { result ->
result.either(
fnL = { Timber.e(it, "Error while upload new url for video block") },
fnR = { Timber.d("Upload Url Success") }
)
}
}
fun onAddVideoFileClicked(filePath: String?) {
if (filePath == null) {
Timber.d("Error while getting filePath")
return
}
uploadUrl.invoke(
scope = viewModelScope,
params = UploadUrl.Params(
contextId = context,
blockId = mediaBlockId,
url = "",
filePath = filePath
)
) { result ->
result.either(
fnL = { Timber.e(it, "Error while upload new file path for video block") },
fnR = { Timber.d("Upload File Path Success") }
)
}
}
fun onChooseVideoFileFromMedia() {
try {
val targetBlock = blocks.first { it.id == mediaBlockId }
val targetContent = targetBlock.content as Content.File
val newContent = targetContent.copy(state = Content.File.State.UPLOADING)
val newBlock = targetBlock.copy(content = newContent)
rerenderingBlocks(newBlock)
} catch (e: Exception) {
Timber.e(e, "Error while update block:$mediaBlockId state to Uploading")
stateData.value = ViewState.Error("Can't load video for this block")
}
}
fun onPageIconClicked() {
val target = blocks.first { it.content is Content.Icon }.id
dispatch(Command.OpenPagePicker(target))
@ -880,7 +987,7 @@ class PageViewModel(
scope = viewModelScope,
params = DownloadFile.Params(
url = urlBuilder.file(file.hash),
name = file.name
name = file.name.orEmpty()
)
) { result ->
result.either(
@ -917,6 +1024,10 @@ class PageViewModel(
data class OpenPagePicker(
val target: String
) : Command()
data class OpenGallery(
val mediaType: String
) : Command()
}
companion object {

View file

@ -30,6 +30,7 @@ open class PageViewModelFactory(
private val updateLinkMarks: UpdateLinkMarks,
private val removeLinkMark: RemoveLinkMark,
private val mergeBlocks: MergeBlocks,
private val uploadUrl: UploadUrl,
private val splitBlock: SplitBlock,
private val documentEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder,
@ -55,6 +56,7 @@ open class PageViewModelFactory(
removeLinkMark = removeLinkMark,
mergeBlocks = mergeBlocks,
splitBlock = splitBlock,
uploadUrl = uploadUrl,
createPage = createPage,
documentExternalEventReducer = documentEventReducer,
urlBuilder = urlBuilder,

View file

@ -37,7 +37,6 @@ object MockBlockFactory {
hash = MockDataFactory.randomUuid(),
name = MockDataFactory.randomString(),
state = Block.Content.File.State.DONE,
added = MockDataFactory.randomLong(),
mime = MockDataFactory.randomString(),
size = MockDataFactory.randomLong(),
type = Block.Content.File.Type.FILE

View file

@ -0,0 +1,183 @@
package com.agileburo.anytype.presentation.mapper
import MockDataFactory
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.misc.UrlBuilder
import org.junit.Test
import kotlin.test.assertEquals
class MapperExtensionKtTest {
@Test
fun `should return block view with type video`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val name = "name"
val size = 10000L
val mime = "video/mp4"
val hash = "647tyhfgehf7ru"
val state = Block.Content.File.State.DONE
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = name,
size = size,
mime = mime,
hash = hash,
state = state,
type = type
)
val expected = BlockView.Video(
id = id,
name = name,
size = size,
mime = mime,
hash = hash,
url = urlBuilder.video(hash)
)
val actual = block.toVideoView(id, urlBuilder)
assertEquals(expected, actual)
}
@Test
fun `should return block view with type video and empty params`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val state = Block.Content.File.State.DONE
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = null,
size = null,
mime = null,
hash = null,
state = state,
type = type
)
val expected = BlockView.Video(
id = id,
name = null,
size = null,
mime = null,
hash = null,
url = urlBuilder.video(null)
)
val actual = block.toVideoView(id, urlBuilder)
assertEquals(expected, actual)
}
@Test
fun `should return block view with type empty`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val state = Block.Content.File.State.EMPTY
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = null,
size = null,
mime = null,
hash = null,
state = state,
type = type
)
val expected = BlockView.VideoEmpty(
id = id
)
val actual = block.toVideoView(id, urlBuilder)
assertEquals(expected, actual)
}
@Test
fun `should return block view with type upload`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val state = Block.Content.File.State.UPLOADING
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = null,
size = null,
mime = null,
hash = null,
state = state,
type = type
)
val expected = BlockView.VideoUpload(
id = id
)
val actual = block.toVideoView(id, urlBuilder)
assertEquals(expected, actual)
}
@Test
fun `should return block view with type error`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val state = Block.Content.File.State.ERROR
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = null,
size = null,
mime = null,
hash = null,
state = state,
type = type
)
val expected = BlockView.VideoError(
id = id
)
val actual = block.toVideoView(id, urlBuilder)
assertEquals(expected, actual)
}
@Test(expected = NotImplementedError::class)
fun `should throw NotImplementedError when state null`() {
val id = MockDataFactory.randomUuid()
val urlBuilder = UrlBuilder(config = Config(home = "home", gateway = "gateway"))
val type = Block.Content.File.Type.VIDEO
val block = Block.Content.File(
name = null,
size = null,
mime = null,
hash = null,
state = null,
type = type
)
block.toVideoView(id, urlBuilder)
}
}

View file

@ -97,6 +97,9 @@ class PageViewModelTest {
@Mock
lateinit var downloadFile: DownloadFile
@Mock
lateinit var uploadUrl: UploadUrl
@Mock
lateinit var emojifier: Emojifier
@ -2757,7 +2760,7 @@ class PageViewModelTest {
scope = any(),
params = eq(
DownloadFile.Params(
name = file.content<Block.Content.File>().name,
name = file.content<Block.Content.File>().name.orEmpty(),
url = builder.file(
hash = file.content<Block.Content.File>().hash
)
@ -2845,7 +2848,8 @@ class PageViewModelTest {
documentExternalEventReducer = DocumentExternalEventReducer(),
urlBuilder = urlBuilder,
downloadFile = downloadFile,
emojifier = emojifier
emojifier = emojifier,
uploadUrl = uploadUrl
)
}
}

View file

@ -32,17 +32,27 @@ android {
dependencies {
def applicationDependencies = rootProject.ext.mainApplication
implementation 'com.github.HBiSoft:PickiT:0.1.9'
implementation 'com.vdurmont:emoji-java:5.1.1'
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-page-icon-picker-widget')
implementation applicationDependencies.timber
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation applicationDependencies.permissionDisp
kapt applicationDependencies.permissionDispCompiler
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

View file

@ -3,17 +3,23 @@
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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".SampleApp"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity" />
<activity android:name=".PageIconPickerSampleActivity">
<activity android:name=".files.LocalFileActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View file

@ -0,0 +1,21 @@
package com.agileburo.anytype.sample
import android.app.Application
import com.agileburo.anytype.core_utils.tools.CrashlyticsTree
import timber.log.Timber
class SampleApp : Application(){
override fun onCreate() {
super.onCreate()
setupTimber()
}
private fun setupTimber() {
if (BuildConfig.DEBUG)
Timber.plant(Timber.DebugTree())
else
Timber.plant(CrashlyticsTree())
}
}