diff --git a/CHANGELOG.md b/CHANGELOG.md
index 187ff88f66..8825951a2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
### New features 🚀
-*
+* Select text and copy-paste inside Anytype. First iteration (#467)
+* Copy and paste multiple blocks in multi-select mode. First iteration (#467)
### Design & UX 🔳
@@ -12,7 +13,8 @@
### Fixes & tech 🚒
-*
+* Resolve race conditions on split and merge (#463, #448)
+* Turn-into code block in edit-mode and multi-select mode does not work (#468)
### Middleware ⚙️
diff --git a/app/build.gradle b/app/build.gradle
index 87b9361ac1..0fa2807e63 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -79,6 +79,7 @@ dependencies {
implementation project(':persistence')
implementation project(':middleware')
implementation project(':presentation')
+ implementation project(':clipboard')
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-kanban-widget')
diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt
index efbef142ca..7e608fc460 100644
--- a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt
+++ b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt
@@ -5,6 +5,9 @@ import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.core_utils.tools.Counter
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.repo.BlockRepository
+import com.agileburo.anytype.domain.clipboard.Clipboard
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.download.Downloader
import com.agileburo.anytype.domain.event.interactor.EventChannel
@@ -323,7 +326,8 @@ class PageModule {
updateAlignment: UpdateAlignment,
textInteractor: Interactor.TextInteractor,
setupBookmark: SetupBookmark,
- paste: Clipboard.Paste,
+ copy: Copy,
+ paste: Paste,
undo: Undo,
redo: Redo
): Orchestrator = Orchestrator(
@@ -348,7 +352,8 @@ class PageModule {
updateText = updateText,
updateAlignment = updateAlignment,
setupBookmark = setupBookmark,
- paste = paste
+ paste = paste,
+ copy = copy
)
@Provides
@@ -390,8 +395,22 @@ class PageModule {
@Provides
@PerScreen
fun provideClipboardPasteUseCase(
- repo: BlockRepository
- ) : Clipboard.Paste = Clipboard.Paste(
- repo = repo
+ repo: BlockRepository,
+ clipboard: Clipboard,
+ matcher: Clipboard.UriMatcher
+ ) : Paste = Paste(
+ repo = repo,
+ clipboard = clipboard,
+ matcher = matcher
+ )
+
+ @Provides
+ @PerScreen
+ fun provideCopyUseCase(
+ repo: BlockRepository,
+ clipboard: Clipboard
+ ) : Copy = Copy(
+ repo = repo,
+ clipboard = clipboard
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt
new file mode 100644
index 0000000000..cd67cfa0d9
--- /dev/null
+++ b/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt
@@ -0,0 +1,63 @@
+package com.agileburo.anytype.di.main
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Context.CLIPBOARD_SERVICE
+import com.agileburo.anytype.clipboard.AnytypeClipboard
+import com.agileburo.anytype.clipboard.AnytypeClipboardStorage
+import com.agileburo.anytype.clipboard.AnytypeUriMatcher
+import com.agileburo.anytype.data.auth.mapper.Serializer
+import com.agileburo.anytype.data.auth.other.ClipboardDataUriMatcher
+import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataRepository
+import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore
+import com.agileburo.anytype.domain.clipboard.Clipboard
+import com.agileburo.anytype.middleware.converters.ClipboardSerializer
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class ClipboardModule {
+
+ @Provides
+ @Singleton
+ fun provideClipboardRepository(
+ factory: ClipboardDataStore.Factory
+ ) : Clipboard = ClipboardDataRepository(factory)
+
+ @Provides
+ @Singleton
+ fun provideClipboardDataStoreFactory(
+ storage: ClipboardDataStore.Storage,
+ system: ClipboardDataStore.System
+ ) : ClipboardDataStore.Factory = ClipboardDataStore.Factory(storage, system)
+
+ @Provides
+ @Singleton
+ fun provideClipboardStorage(
+ context: Context,
+ serializer: Serializer
+ ) : ClipboardDataStore.Storage = AnytypeClipboardStorage(context, serializer)
+
+ @Provides
+ @Singleton
+ fun provideClipboardSystem(
+ cm: ClipboardManager
+ ) : ClipboardDataStore.System = AnytypeClipboard(cm)
+
+ @Provides
+ @Singleton
+ fun provideClipboardManager(
+ context: Context
+ ) : ClipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+
+ @Provides
+ @Singleton
+ fun provideUriMatcher() : Clipboard.UriMatcher = ClipboardDataUriMatcher(
+ matcher = AnytypeUriMatcher()
+ )
+
+ @Provides
+ @Singleton
+ fun provideSerializer() : Serializer = ClipboardSerializer()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt
index da5cf561ac..4d82095bba 100644
--- a/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt
+++ b/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt
@@ -4,7 +4,7 @@ import android.content.Context
import com.agileburo.anytype.data.auth.other.DataDownloader
import com.agileburo.anytype.data.auth.other.Device
import com.agileburo.anytype.device.base.AndroidDevice
-import com.agileburo.anytype.device.download.DeviceDownloader
+import com.agileburo.anytype.device.download.AndroidDeviceDownloader
import com.agileburo.anytype.domain.download.Downloader
import dagger.Module
import dagger.Provides
@@ -22,13 +22,13 @@ class DeviceModule {
@Provides
@Singleton
fun provideDevice(
- downloader: DeviceDownloader
+ downloader: AndroidDeviceDownloader
): Device = AndroidDevice(downloader = downloader)
@Provides
@Singleton
fun provideDeviceDownloader(
context: Context
- ): DeviceDownloader = DeviceDownloader(context = context)
+ ): AndroidDeviceDownloader = AndroidDeviceDownloader(context = context)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt
index dc71dc7ce1..561ae9b365 100644
--- a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt
+++ b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt
@@ -13,7 +13,8 @@ import javax.inject.Singleton
ConfigModule::class,
DeviceModule::class,
UtilModule::class,
- EmojiModule::class
+ EmojiModule::class,
+ ClipboardModule::class
]
)
interface MainComponent {
diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt
index 21c3010aec..f0654cd50f 100644
--- a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt
+++ b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt
@@ -30,6 +30,7 @@ import com.agileburo.anytype.core_ui.menu.DocumentPopUpMenu
import com.agileburo.anytype.core_ui.model.UiBlock
import com.agileburo.anytype.core_ui.reactive.clicks
import com.agileburo.anytype.core_ui.state.ControlPanelState
+import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
import com.agileburo.anytype.core_ui.tools.FirstItemInvisibilityDetector
import com.agileburo.anytype.core_ui.tools.OutsideClickDetector
import com.agileburo.anytype.core_ui.widgets.ActionItemType
@@ -70,6 +71,7 @@ open class PageFragment :
OnFragmentInteractionListener,
AddBlockFragment.AddBlockActionReceiver,
TurnIntoActionReceiver,
+ ClipboardInterceptor,
PickiTCallbacks {
private val vm by lazy {
@@ -139,22 +141,7 @@ open class PageFragment :
onLongClickListener = vm::onBlockLongPressedClicked,
onTitleTextInputClicked = vm::onTitleTextInputClicked,
onClickListener = vm::onClickListener,
- clipboardDetector = { range ->
- // TODO this logic should be moved to device module
- clipboard().primaryClip?.let { clip ->
- if (clip.itemCount > 0) {
- val item = clip.getItemAt(0)
- vm.onPaste(
- plain = item.text.toString(),
- html = if (item.htmlText != null)
- item.htmlText
- else
- null,
- range = range
- )
- }
- }
- }
+ clipboardInterceptor = this
)
}
@@ -330,6 +317,11 @@ open class PageFragment :
.onEach { vm.onMultiSelectModeDeleteClicked() }
.launchIn(lifecycleScope)
+ bottomMenu
+ .copyClicks()
+ .onEach { vm.onMultiSelectCopyClicked() }
+ .launchIn(lifecycleScope)
+
bottomMenu
.turnIntoClicks()
.onEach { vm.onMultiSelectTurnIntoButtonClicked() }
@@ -641,6 +633,13 @@ open class PageFragment :
}
}
+ override fun onClipboardAction(action: ClipboardInterceptor.Action) {
+ when(action) {
+ is ClipboardInterceptor.Action.Copy -> vm.onCopy(action.selection)
+ is ClipboardInterceptor.Action.Paste -> vm.onPaste(action.selection)
+ }
+ }
+
private fun showSelectButton() {
ObjectAnimator.ofFloat(
select,
diff --git a/app/src/main/res/layout/fragment_page.xml b/app/src/main/res/layout/fragment_page.xml
index cf3fb86805..29fd8b2f0b 100644
--- a/app/src/main/res/layout/fragment_page.xml
+++ b/app/src/main/res/layout/fragment_page.xml
@@ -24,6 +24,7 @@
app:layout_behavior="@string/bottom_sheet_behavior">
diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt
new file mode 100644
index 0000000000..8a11e64be9
--- /dev/null
+++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt
@@ -0,0 +1,56 @@
+package com.agileburo.anytype.clipboard
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.net.Uri
+import com.agileburo.anytype.clipboard.BuildConfig.ANYTYPE_CLIPBOARD_LABEL
+import com.agileburo.anytype.clipboard.BuildConfig.ANYTYPE_CLIPBOARD_URI
+
+import com.agileburo.anytype.data.auth.model.ClipEntity
+import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore
+
+class AnytypeClipboard(
+ private val cm: ClipboardManager
+) : ClipboardDataStore.System {
+
+ override suspend fun put(text: String, html: String?) {
+
+ val uri = Uri.parse(ANYTYPE_CLIPBOARD_URI)
+ if (html != null)
+ cm.setPrimaryClip(
+ ClipData.newHtmlText(ANYTYPE_CLIPBOARD_LABEL, text, html).apply {
+ addItem(ClipData.Item(uri))
+ }
+ )
+ else
+ cm.setPrimaryClip(
+ ClipData.newPlainText(ANYTYPE_CLIPBOARD_LABEL, text).apply {
+ addItem(ClipData.Item(uri))
+ }
+ )
+ }
+
+ override suspend fun clip(): ClipEntity? {
+ return cm.primaryClip?.let { clip ->
+ when {
+ clip.itemCount > 1 -> {
+ ClipEntity(
+ text = clip.getItemAt(0).text.toString(),
+ html = clip.getItemAt(0).htmlText,
+ uri = clip.getItemAt(1).uri.toString()
+ )
+ }
+ clip.itemCount == 1 -> {
+ ClipEntity(
+ text = clip.getItemAt(0).text.toString(),
+ html = clip.getItemAt(0).htmlText,
+ uri = null
+ )
+ }
+ else -> {
+ null
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt
new file mode 100644
index 0000000000..e6be261ab8
--- /dev/null
+++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt
@@ -0,0 +1,30 @@
+package com.agileburo.anytype.clipboard
+
+import android.content.Context
+import com.agileburo.anytype.data.auth.mapper.Serializer
+import com.agileburo.anytype.data.auth.model.BlockEntity
+import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore
+
+class AnytypeClipboardStorage(
+ private val context: Context,
+ private val serializer: Serializer
+) : ClipboardDataStore.Storage {
+
+ override fun persist(blocks: List) {
+ val serialized = serializer.serialize(blocks)
+ context.openFileOutput(CLIPBOARD_FILE_NAME, Context.MODE_PRIVATE).use {
+ it.write(serialized)
+ it.flush()
+ }
+ }
+
+ override fun fetch(): List {
+ val stream = context.openFileInput(CLIPBOARD_FILE_NAME)
+ val blob = stream.use { it.readBytes() }
+ return serializer.deserialize(blob)
+ }
+
+ companion object {
+ const val CLIPBOARD_FILE_NAME = "anytype_clipboard"
+ }
+}
\ No newline at end of file
diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt
new file mode 100644
index 0000000000..99dc165bed
--- /dev/null
+++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt
@@ -0,0 +1,9 @@
+package com.agileburo.anytype.clipboard
+
+import com.agileburo.anytype.data.auth.other.ClipboardUriMatcher
+
+class AnytypeUriMatcher : ClipboardUriMatcher {
+ override fun isAnytypeUri(uri: String): Boolean {
+ return uri == BuildConfig.ANYTYPE_CLIPBOARD_URI
+ }
+}
\ No newline at end of file
diff --git a/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt b/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt
new file mode 100644
index 0000000000..088d14299e
--- /dev/null
+++ b/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt
@@ -0,0 +1,77 @@
+package com.agileburo.anytype.clipboard
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+@Config(sdk = [Build.VERSION_CODES.P])
+@RunWith(RobolectricTestRunner::class)
+class AndroidClipboardTest {
+
+ private lateinit var clipboard : AnytypeClipboard
+
+ private lateinit var cm: ClipboardManager
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext()
+ cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ clipboard = AnytypeClipboard(cm = cm)
+ }
+
+ @Test
+ fun `should put only text and uri`() {
+ val text = MockDataFactory.randomString()
+
+ runBlocking {
+ clipboard.put(
+ text = text,
+ html = null
+ )
+ }
+
+ assertEquals(
+ expected = text,
+ actual = cm.primaryClip?.getItemAt(0)?.text
+ )
+
+ assertNull(cm.primaryClip?.getItemAt(0)?.htmlText)
+ assertNotNull(cm.primaryClip?.getItemAt(1)?.uri)
+ }
+
+ @Test
+ fun `should put text, html as first item and uri as second item`() {
+ val text = MockDataFactory.randomString()
+ val html = MockDataFactory.randomString()
+
+ runBlocking {
+ clipboard.put(
+ text = text,
+ html = html
+ )
+ }
+
+ assertEquals(
+ expected = text,
+ actual = cm.primaryClip?.getItemAt(0)?.text
+ )
+
+ assertEquals(
+ expected = html,
+ actual = cm.primaryClip?.getItemAt(0)?.htmlText
+ )
+
+ assertNull(cm.primaryClip?.getItemAt(0)?.uri)
+ assertNotNull(cm.primaryClip?.getItemAt(1)?.uri)
+ }
+}
\ No newline at end of file
diff --git a/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt b/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt
new file mode 100644
index 0000000000..4e8c3536c0
--- /dev/null
+++ b/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt
@@ -0,0 +1,63 @@
+package com.agileburo.anytype.clipboard
+
+import java.util.*
+import java.util.concurrent.ThreadLocalRandom
+
+object MockDataFactory {
+
+ fun randomUuid(): String {
+ return UUID.randomUUID().toString()
+ }
+
+ fun randomString(): String {
+ return randomUuid()
+ }
+
+ fun randomInt(): Int {
+ return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
+ }
+
+ fun randomInt(max: Int): Int {
+ return ThreadLocalRandom.current().nextInt(0, max)
+ }
+
+ fun randomLong(): Long {
+ return randomInt().toLong()
+ }
+
+ fun randomFloat(): Float {
+ return randomInt().toFloat()
+ }
+
+ fun randomDouble(): Double {
+ return randomInt().toDouble()
+ }
+
+ fun randomBoolean(): Boolean {
+ return Math.random() < 0.5
+ }
+
+ fun makeIntList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomInt())
+ }
+ return items
+ }
+
+ fun makeStringList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomUuid())
+ }
+ return items
+ }
+
+ fun makeDoubleList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomDouble())
+ }
+ return items
+ }
+}
\ No newline at end of file
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index 9531127d6d..15c7322219 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -62,4 +62,5 @@ dependencies {
testImplementation unitTestDependencies.kotlinTest
testImplementation unitTestDependencies.robolectric
testImplementation unitTestDependencies.androidXTestCore
+ testImplementation unitTestDependencies.mockitoKotlin
}
\ No newline at end of file
diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt
index d61fd04e10..f2ad3384f5 100644
--- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt
+++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt
@@ -38,6 +38,7 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOL
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_PLACEHOLDER
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_UPLOAD
+import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
import com.agileburo.anytype.core_utils.ext.typeOf
import timber.log.Timber
@@ -72,7 +73,7 @@ class BlockAdapter(
private val onToggleClicked: (String) -> Unit,
private val onMarkupActionClicked: (Markup.Type) -> Unit,
private val onLongClickListener: (String) -> Unit,
- private val clipboardDetector: (IntRange) -> Unit
+ private val clipboardInterceptor: ClipboardInterceptor
) : RecyclerView.Adapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder {
@@ -844,9 +845,7 @@ class BlockAdapter(
else
holder.setOnClickListener { onTextInputClicked(blocks[holder.adapterPosition].id) }
- holder.content.clipboardDetector = {
- clipboardDetector(holder.content.selectionStart..holder.content.selectionEnd)
- }
+ holder.content.clipboardInterceptor = clipboardInterceptor
}
}
diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt
new file mode 100644
index 0000000000..b4d3b55bbe
--- /dev/null
+++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt
@@ -0,0 +1,11 @@
+package com.agileburo.anytype.core_ui.tools
+
+interface ClipboardInterceptor {
+
+ fun onClipboardAction(action: Action)
+
+ sealed class Action {
+ data class Copy(val selection: IntRange) : Action()
+ data class Paste(val selection: IntRange) : Action()
+ }
+}
\ No newline at end of file
diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt
index 3c0024b95c..ff05049b44 100644
--- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt
+++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt
@@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.graphics.withTranslation
import com.agileburo.anytype.core_ui.extensions.toast
+import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
import com.agileburo.anytype.core_ui.tools.DefaultTextWatcher
import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightAttributeReader
import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightDrawer
@@ -28,10 +29,12 @@ class TextInputWidget : AppCompatEditText {
}
private val watchers: MutableList = mutableListOf()
+
private var highlightDrawer: HighlightDrawer? = null
var selectionDetector: ((IntRange) -> Unit)? = null
- var clipboardDetector: (() -> Unit)? = null
+
+ var clipboardInterceptor: ClipboardInterceptor? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
@@ -121,19 +124,40 @@ class TextInputWidget : AppCompatEditText {
}
override fun onTextContextMenuItem(id: Int): Boolean {
- var consumed = true
+ if (clipboardInterceptor == null) {
+ return super.onTextContextMenuItem(id)
+ }
+
+ var consumed = false
+
when(id) {
R.id.paste -> {
- clipboardDetector?.invoke()
- }
- R.id.cut -> {
- consumed = super.onTextContextMenuItem(id)
+ if (clipboardInterceptor != null) {
+ clipboardInterceptor?.onClipboardAction(
+ ClipboardInterceptor.Action.Paste(
+ selection = selectionStart..selectionEnd
+ )
+ )
+ consumed = true
+ }
}
R.id.copy -> {
- consumed = super.onTextContextMenuItem(id)
+ if (clipboardInterceptor != null) {
+ clipboardInterceptor?.onClipboardAction(
+ ClipboardInterceptor.Action.Copy(
+ selection = selectionStart..selectionEnd
+ )
+ )
+ consumed = true
+ }
}
}
- return consumed
+
+ return if (!consumed) {
+ super.onTextContextMenuItem(id)
+ } else {
+ consumed
+ }
}
override fun onDraw(canvas: Canvas?) {
diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt
index 655f2007a8..82fb551002 100644
--- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt
+++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt
@@ -39,6 +39,9 @@ class MultiSelectBottomToolbarWidget : ConstraintLayout {
fun deleteClicks() = delete.clicks()
fun turnIntoClicks() = convert.clicks()
+ // Temporary button usage for copying.
+ fun copyClicks() = more.clicks()
+
fun showWithAnimation() {
ObjectAnimator.ofFloat(this, ANIMATED_PROPERTY, 0f).apply {
duration = ANIMATION_DURATION
diff --git a/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml b/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml
index 3e77311260..8c0eb834a3 100644
--- a/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml
+++ b/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml
@@ -3,10 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+ android:text="@string/copy" />
@@ -88,12 +90,12 @@
diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml
index c1ece95a76..78ad980ca6 100644
--- a/core-ui/src/main/res/values/strings.xml
+++ b/core-ui/src/main/res/values/strings.xml
@@ -197,5 +197,6 @@
Undo
Redo
+ Copy
diff --git a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt
index c5e0ca724a..3d875072cf 100644
--- a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt
+++ b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt
@@ -23,9 +23,11 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.T
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_COLOR_CHANGED
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.FOCUS_TIMEOUT_MILLIS
+import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
import com.agileburo.anytype.core_ui.widgets.text.TextInputWidget.Companion.TEXT_INPUT_WIDGET_INPUT_TYPE
import com.agileburo.anytype.core_utils.ext.dimen
import com.agileburo.anytype.core_utils.ext.hexColorCode
+import com.nhaarman.mockitokotlin2.mock
import kotlinx.android.synthetic.main.item_block_bookmark_placeholder.view.*
import kotlinx.android.synthetic.main.item_block_checkbox.view.*
import kotlinx.android.synthetic.main.item_block_page.view.*
@@ -44,6 +46,8 @@ class BlockAdapterTest {
private val context: Context = ApplicationProvider.getApplicationContext()
+ private val clipboardInterceptor : ClipboardInterceptor = mock()
+
@Test
fun `should return transparent hex code when int color value is zero`() {
@@ -3263,7 +3267,7 @@ class BlockAdapterTest {
onLongClickListener = {},
onTitleTextInputClicked = {},
onClickListener = {},
- clipboardDetector = {}
+ clipboardInterceptor = clipboardInterceptor
)
}
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt
index 6d1043681a..63f241c266 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt
@@ -3,10 +3,11 @@ package com.agileburo.anytype.data.auth.mapper
import com.agileburo.anytype.data.auth.model.*
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.model.Wallet
-import com.agileburo.anytype.domain.block.interactor.Clipboard
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.model.Position
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.event.model.Payload
@@ -49,10 +50,8 @@ fun BlockEntity.Details.toDomain(): Block.Details = Block.Details(
fun BlockEntity.Content.toDomain(): Block.Content = when (this) {
is BlockEntity.Content.Text -> toDomain()
- is BlockEntity.Content.Dashboard -> toDomain()
is BlockEntity.Content.Page -> toDomain()
is BlockEntity.Content.Layout -> toDomain()
- is BlockEntity.Content.Image -> toDomain()
is BlockEntity.Content.Link -> toDomain()
is BlockEntity.Content.Divider -> toDomain()
is BlockEntity.Content.File -> toDomain()
@@ -116,12 +115,6 @@ fun BlockEntity.Content.Text.toDomain(): Block.Content.Text {
)
}
-fun BlockEntity.Content.Dashboard.toDomain(): Block.Content.Dashboard {
- return Block.Content.Dashboard(
- type = Block.Content.Dashboard.Type.valueOf(type.name)
- )
-}
-
fun BlockEntity.Content.Page.toDomain(): Block.Content.Page {
return Block.Content.Page(
style = Block.Content.Page.Style.valueOf(style.name)
@@ -148,12 +141,6 @@ fun Block.Content.Layout.toEntity(): BlockEntity.Content.Layout {
)
}
-fun BlockEntity.Content.Image.toDomain(): Block.Content.Image {
- return Block.Content.Image(
- path = path
- )
-}
-
fun BlockEntity.Content.Divider.toDomain() = Block.Content.Divider
@@ -161,12 +148,6 @@ fun BlockEntity.Content.Smart.toDomain() = Block.Content.Smart(
type = Block.Content.Smart.Type.valueOf(type.name)
)
-fun Block.Content.Image.toEntity(): BlockEntity.Content.Image {
- return BlockEntity.Content.Image(
- path = path
- )
-}
-
fun BlockEntity.Content.Text.Mark.toDomain(): Block.Content.Text.Mark {
return Block.Content.Text.Mark(
range = range,
@@ -186,10 +167,8 @@ fun Block.toEntity(): BlockEntity {
fun Block.Content.toEntity(): BlockEntity.Content = when (this) {
is Block.Content.Text -> toEntity()
- is Block.Content.Dashboard -> toEntity()
is Block.Content.Page -> toEntity()
is Block.Content.Layout -> toEntity()
- is Block.Content.Image -> toEntity()
is Block.Content.Link -> toEntity()
is Block.Content.Divider -> toEntity()
is Block.Content.File -> toEntity()
@@ -249,12 +228,6 @@ fun Block.Content.Text.toEntity(): BlockEntity.Content.Text {
)
}
-fun Block.Content.Dashboard.toEntity(): BlockEntity.Content.Dashboard {
- return BlockEntity.Content.Dashboard(
- type = BlockEntity.Content.Dashboard.Type.valueOf(type.name)
- )
-}
-
fun Block.Content.Page.toEntity(): BlockEntity.Content.Page {
return BlockEntity.Content.Page(
style = BlockEntity.Content.Page.Style.valueOf(style.name)
@@ -404,6 +377,12 @@ fun Command.Paste.toEntity() = CommandEntity.Paste(
range = range
)
+fun Command.Copy.toEntity() = CommandEntity.Copy(
+ context = context,
+ blocks = blocks.map { it.toEntity() },
+ range = range
+)
+
fun Command.CreateDocument.toEntity() = CommandEntity.CreateDocument(
context = context,
target = target,
@@ -573,8 +552,14 @@ fun BlockEntity.Align.toDomain(): Block.Align = when (this) {
BlockEntity.Align.AlignRight -> Block.Align.AlignRight
}
-fun Response.Clipboard.Paste.toDomain() = Clipboard.Paste.Response(
+fun Response.Clipboard.Paste.toDomain() = Paste.Response(
blocks = blocks,
cursor = cursor,
payload = payload.toDomain()
+)
+
+fun Response.Clipboard.Copy.toDomain() = Copy.Response(
+ text = plain,
+ html = html,
+ blocks = blocks.map { it.toDomain() }
)
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt
new file mode 100644
index 0000000000..936ce2fb46
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt
@@ -0,0 +1,8 @@
+package com.agileburo.anytype.data.auth.mapper
+
+import com.agileburo.anytype.data.auth.model.BlockEntity
+
+interface Serializer {
+ fun serialize(blocks: List) : ByteArray
+ fun deserialize(blob: ByteArray) : List
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt
index bd5252204a..df3b7c6178 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt
@@ -56,18 +56,10 @@ data class BlockEntity(
enum class Type { ROW, COLUMN, DIV }
}
- data class Image(
- val path: String
- ) : Content()
-
data class Icon(
val name: String
) : Content()
- data class Dashboard(val type: Type) : Content() {
- enum class Type { MAIN_SCREEN, ARCHIVE }
- }
-
data class Page(val style: Style) : Content() {
enum class Style { EMPTY, TASK, SET }
}
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt
new file mode 100644
index 0000000000..3fa4ff1c81
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt
@@ -0,0 +1,12 @@
+package com.agileburo.anytype.data.auth.model
+
+import com.agileburo.anytype.domain.clipboard.Clip
+
+/**
+ * @see Clip
+ */
+class ClipEntity(
+ override val text: String,
+ override val html: String?,
+ override val uri: String?
+) : Clip
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt
index fe19328c70..0d82902b85 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt
@@ -133,4 +133,10 @@ class CommandEntity {
val html: String?,
val blocks: List
)
+
+ data class Copy(
+ val context: String,
+ val range: IntRange?,
+ val blocks: List
+ )
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt
index 862c5d2c50..fe6bd93f4b 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt
@@ -2,10 +2,16 @@ package com.agileburo.anytype.data.auth.model
sealed class Response {
sealed class Clipboard : Response() {
- data class Paste(
+ class Paste(
val cursor: Int,
val blocks: List,
val payload: PayloadEntity
) : Clipboard()
+
+ class Copy(
+ val plain: String,
+ val html: String?,
+ val blocks: List
+ ) : Clipboard()
}
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt
new file mode 100644
index 0000000000..4bef2eb82e
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt
@@ -0,0 +1,12 @@
+package com.agileburo.anytype.data.auth.other
+
+import com.agileburo.anytype.domain.clipboard.Clipboard
+
+class ClipboardDataUriMatcher(
+ private val matcher: ClipboardUriMatcher
+) : Clipboard.UriMatcher {
+
+ override fun isAnytypeUri(
+ uri: String
+ ): Boolean = matcher.isAnytypeUri(uri)
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt
new file mode 100644
index 0000000000..3ec9b0d3b3
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt
@@ -0,0 +1,5 @@
+package com.agileburo.anytype.data.auth.other
+
+interface ClipboardUriMatcher {
+ fun isAnytypeUri(uri: String) : Boolean
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt
index 283156541c..af3f6d555a 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt
@@ -2,9 +2,10 @@ package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.mapper.toDomain
import com.agileburo.anytype.data.auth.mapper.toEntity
-import com.agileburo.anytype.domain.block.interactor.Clipboard
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
@@ -126,5 +127,9 @@ class BlockDataRepository(
override suspend fun paste(
command: Command.Paste
- ): Clipboard.Paste.Response = factory.remote.paste(command.toEntity()).toDomain()
+ ): Paste.Response = factory.remote.paste(command.toEntity()).toDomain()
+
+ override suspend fun copy(
+ command: Command.Copy
+ ): Copy.Response = factory.remote.copy(command.toEntity()).toDomain()
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt
index 346dce84ac..16548c10d2 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt
@@ -40,4 +40,5 @@ interface BlockDataStore {
suspend fun redo(command: CommandEntity.Redo) : PayloadEntity
suspend fun archiveDocument(command: CommandEntity.ArchiveDocument)
suspend fun paste(command: CommandEntity.Paste) : Response.Clipboard.Paste
+ suspend fun copy(command: CommandEntity.Copy) : Response.Clipboard.Copy
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt
index 86ccd46c1e..7559b65d13 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt
@@ -40,4 +40,5 @@ interface BlockRemote {
suspend fun redo(command: CommandEntity.Redo) : PayloadEntity
suspend fun archiveDocument(command: CommandEntity.ArchiveDocument)
suspend fun paste(command: CommandEntity.Paste) : Response.Clipboard.Paste
+ suspend fun copy(command: CommandEntity.Copy) : Response.Clipboard.Copy
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt
index 2eeb899f4d..cfd9b87c2b 100644
--- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt
@@ -109,4 +109,8 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
override suspend fun paste(
command: CommandEntity.Paste
): Response.Clipboard.Paste = remote.paste(command)
+
+ override suspend fun copy(
+ command: CommandEntity.Copy
+ ): Response.Clipboard.Copy = remote.copy(command)
}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt
new file mode 100644
index 0000000000..fa6c9f1816
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt
@@ -0,0 +1,28 @@
+package com.agileburo.anytype.data.auth.repo.clipboard
+
+import com.agileburo.anytype.data.auth.mapper.toDomain
+import com.agileburo.anytype.data.auth.mapper.toEntity
+import com.agileburo.anytype.domain.block.model.Block
+import com.agileburo.anytype.domain.clipboard.Clip
+import com.agileburo.anytype.domain.clipboard.Clipboard
+
+class ClipboardDataRepository(
+ private val factory: ClipboardDataStore.Factory
+) : Clipboard {
+
+ override suspend fun put(text: String, html: String?, blocks: List) {
+ factory.storage.persist(
+ blocks = blocks.map { it.toEntity() }
+ )
+ factory.system.put(
+ text = text,
+ html = html
+ )
+ }
+
+ override suspend fun blocks(): List {
+ return factory.storage.fetch().map { it.toDomain() }
+ }
+
+ override suspend fun clip(): Clip? = factory.system.clip()
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt
new file mode 100644
index 0000000000..052cc8ab96
--- /dev/null
+++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt
@@ -0,0 +1,35 @@
+package com.agileburo.anytype.data.auth.repo.clipboard
+
+import com.agileburo.anytype.data.auth.model.BlockEntity
+import com.agileburo.anytype.data.auth.model.ClipEntity
+
+interface ClipboardDataStore {
+ /**
+ * Stores last copied Anytype blocks.
+ * @see ClipEntity
+ */
+ interface Storage {
+ fun persist(blocks: List)
+ fun fetch() : List
+ }
+
+ /**
+ * Provides access to system clipboard.
+ */
+ interface System {
+ /**
+ * Stores copied [text] and optionally a [html] representation on the systen clipboard.
+ */
+ suspend fun put(text: String, html: String?)
+
+ /**
+ * @return current clip on the clipboard.
+ */
+ suspend fun clip() : ClipEntity?
+ }
+
+ class Factory(
+ val storage: Storage,
+ val system: System
+ )
+}
\ No newline at end of file
diff --git a/device/build.gradle b/device/build.gradle
index d5ef4fa03b..36f4280e8d 100644
--- a/device/build.gradle
+++ b/device/build.gradle
@@ -53,4 +53,6 @@ dependencies {
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest
+ testImplementation unitTestDependencies.androidXTestCore
+ testImplementation unitTestDependencies.robolectric
}
\ No newline at end of file
diff --git a/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt b/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt
index 0876df418d..2afdeb8f6f 100644
--- a/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt
+++ b/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt
@@ -1,10 +1,11 @@
package com.agileburo.anytype.device.base
import com.agileburo.anytype.data.auth.other.Device
-import com.agileburo.anytype.device.download.DeviceDownloader
-
-class AndroidDevice(private val downloader: DeviceDownloader) : Device {
+import com.agileburo.anytype.device.download.AndroidDeviceDownloader
+class AndroidDevice(
+ private val downloader: AndroidDeviceDownloader
+) : Device {
override fun download(url: String, name: String) {
downloader.download(url = url, name = name)
}
diff --git a/device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt b/device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt
similarity index 94%
rename from device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt
rename to device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt
index 8167172348..eb2dd1bf1d 100644
--- a/device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt
+++ b/device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt
@@ -8,7 +8,7 @@ import android.net.Uri
import android.os.Environment.DIRECTORY_DOWNLOADS
import timber.log.Timber
-class DeviceDownloader(private val context: Context) {
+class AndroidDeviceDownloader(private val context: Context) {
private val manager by lazy {
context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt
deleted file mode 100644
index 2fafc54997..0000000000
--- a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.agileburo.anytype.domain.block.interactor
-
-import com.agileburo.anytype.domain.base.BaseUseCase
-import com.agileburo.anytype.domain.block.model.Block
-import com.agileburo.anytype.domain.block.model.Command
-import com.agileburo.anytype.domain.block.repo.BlockRepository
-import com.agileburo.anytype.domain.common.Id
-import com.agileburo.anytype.domain.event.model.Payload
-
-interface Clipboard {
-
- /**
- * Use-case for pasting to Anytype clipboard.
- */
- class Paste(
- private val repo: BlockRepository
- ) : BaseUseCase(), Clipboard {
-
- override suspend fun run(params: Params) = safe {
- repo.paste(
- command = Command.Paste(
- context = params.context,
- focus = params.focus,
- selected = params.selected,
- range = params.range,
- text = params.text,
- html = params.html,
- blocks = params.blocks
- )
- )
- }
-
- /**
- * Params for pasting to Anytype clipboard
- * @property context id of the context
- * @property focus id of the focused/target block
- * @property selected id of currently selected blocks
- * @property range selected text range
- * @property text plain text to paste
- * @property html optional html to paste
- * @property blocks blocks currently contained in clipboard
- */
- data class Params(
- val context: Id,
- val focus: Id,
- val selected: List,
- val range: IntRange,
- val text: String,
- val html: String?,
- val blocks: List
- )
-
- /**
- * Response for [Clipboard.Paste] use-case.
- * @param cursor caret position
- * @param blocks ids of the new blocks
- * @param payload response payload
- */
- data class Response(
- val cursor: Int,
- val blocks: List,
- val payload: Payload
- )
- }
-}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt
index 2f4f0d2090..8c6827d600 100644
--- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt
+++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt
@@ -53,9 +53,6 @@ data class Block(
fun asText() = this as Text
fun asLink() = this as Link
- fun asDashboard() = this as Dashboard
- fun asDivider() = this as Divider
- fun asFile() = this as File
/**
* Smart block.
@@ -135,14 +132,6 @@ data class Block(
enum class Type { ROW, COLUMN, DIV }
}
- data class Image(
- val path: String
- ) : Content()
-
- data class Dashboard(val type: Type) : Content() {
- enum class Type { MAIN_SCREEN, ARCHIVE }
- }
-
data class Page(val style: Style) : Content() {
enum class Style { EMPTY, TASK, SET }
}
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt
index c98263cefd..8bf7d41ae2 100644
--- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt
+++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt
@@ -231,7 +231,7 @@ sealed class Command {
data class Redo(val context: Id)
/**
- * Params for clipboard pasting operation
+ * Command for clipboard paste operation
* @property context id of the context
* @property focus id of the focused/target block
* @property selected id of currently selected blocks
@@ -249,4 +249,16 @@ sealed class Command {
val html: String?,
val blocks: List
)
+
+ /**
+ * Command for clipboard copy operation.
+ * @param context id of the context
+ * @param range selected text range
+ * @param blocks associated blocks
+ */
+ data class Copy(
+ val context: Id,
+ val range: IntRange?,
+ val blocks: List
+ )
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt
index fc47da51e0..2bf7209363 100644
--- a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt
+++ b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt
@@ -1,12 +1,14 @@
package com.agileburo.anytype.domain.block.repo
-import com.agileburo.anytype.domain.block.interactor.Clipboard
import com.agileburo.anytype.domain.block.model.Command
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.event.model.Payload
interface BlockRepository {
+
suspend fun dnd(command: Command.Dnd)
suspend fun unlink(command: Command.Unlink): Payload
@@ -79,5 +81,6 @@ interface BlockRepository {
suspend fun undo(command: Command.Undo) : Payload
suspend fun redo(command: Command.Redo) : Payload
- suspend fun paste(command: Command.Paste) : Clipboard.Paste.Response
+ suspend fun copy(command: Command.Copy) : Copy.Response
+ suspend fun paste(command: Command.Paste) : Paste.Response
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt
new file mode 100644
index 0000000000..0bac9e5aa5
--- /dev/null
+++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt
@@ -0,0 +1,13 @@
+package com.agileburo.anytype.domain.clipboard
+
+/**
+ * A clip on the clipboard.
+ * @property text plain text
+ * @property html html representation
+ * @property uri uri for the copied content (Anytype URI or an external app URI)
+ */
+interface Clip {
+ val text: String
+ val html: String?
+ val uri: String?
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt
new file mode 100644
index 0000000000..525df952d8
--- /dev/null
+++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt
@@ -0,0 +1,29 @@
+package com.agileburo.anytype.domain.clipboard
+
+import com.agileburo.anytype.domain.block.model.Block
+
+interface Clipboard {
+ /**
+ * @param text plain text to put on the clipboard
+ * @param html optional html to put on the clipboard
+ * @param blocks Anytype blocks to store on the clipboard)
+ */
+ suspend fun put(text: String, html: String?, blocks: List)
+
+ /**
+ * @return Anytype blocks currently stored on (or linked to) the clipboard
+ */
+ suspend fun blocks() : List
+
+ /**
+ * @return return current clip on the clipboard
+ */
+ suspend fun clip() : Clip?
+
+ interface UriMatcher {
+ /**
+ * Checks whether this [uri] is internal Anytype clipboard URI.
+ */
+ fun isAnytypeUri(uri: String) : Boolean
+ }
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt
new file mode 100644
index 0000000000..48141006e1
--- /dev/null
+++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt
@@ -0,0 +1,51 @@
+package com.agileburo.anytype.domain.clipboard
+
+import com.agileburo.anytype.domain.base.BaseUseCase
+import com.agileburo.anytype.domain.block.model.Block
+import com.agileburo.anytype.domain.block.model.Command
+import com.agileburo.anytype.domain.block.repo.BlockRepository
+import com.agileburo.anytype.domain.common.Id
+
+class Copy(
+ private val repo: BlockRepository,
+ private val clipboard: Clipboard
+) : BaseUseCase() {
+
+ override suspend fun run(params: Params) = safe {
+ val result = repo.copy(
+ command = Command.Copy(
+ context = params.context,
+ range = params.range,
+ blocks = params.blocks
+ )
+ )
+ clipboard.put(
+ text = result.text,
+ html = result.html,
+ blocks = result.blocks
+ )
+ }
+
+ /**
+ * Params for clipboard paste operation.
+ * @param context id of the context
+ * @param range selected text range
+ * @param blocks associated blocks
+ */
+ data class Params(
+ val context: Id,
+ val range: IntRange?,
+ val blocks: List
+ )
+
+ /**
+ * @param text plain text
+ * @param html optional html
+ * @param blocks anytype clipboard slot
+ */
+ class Response(
+ val text: String,
+ val html: String?,
+ val blocks: List
+ )
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt
new file mode 100644
index 0000000000..2755548064
--- /dev/null
+++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt
@@ -0,0 +1,69 @@
+package com.agileburo.anytype.domain.clipboard
+
+import com.agileburo.anytype.domain.base.BaseUseCase
+import com.agileburo.anytype.domain.block.model.Command
+import com.agileburo.anytype.domain.block.repo.BlockRepository
+import com.agileburo.anytype.domain.common.Id
+import com.agileburo.anytype.domain.event.model.Payload
+
+/**
+ * Use-case for pasting to Anytype clipboard.
+ */
+class Paste(
+ private val repo: BlockRepository,
+ private val clipboard: Clipboard,
+ private val matcher: Clipboard.UriMatcher
+) : BaseUseCase() {
+
+ override suspend fun run(params: Params) = safe {
+ val clip = clipboard.clip()
+
+ if (clip != null) {
+
+ val uri = clip.uri
+
+ val blocks = if (uri != null && matcher.isAnytypeUri(uri))
+ clipboard.blocks()
+ else
+ emptyList()
+
+ repo.paste(
+ command = Command.Paste(
+ context = params.context,
+ focus = params.focus,
+ selected = emptyList(),
+ range = params.range,
+ text = clip.text,
+ html = clip.html,
+ blocks = blocks
+ )
+ )
+ } else {
+ throw IllegalStateException("Empty clip!")
+ }
+ }
+
+ /**
+ * Params for pasting to Anytype clipboard
+ * @property context id of the context
+ * @property focus id of the focused/target block
+ * @property range selected text range
+ */
+ data class Params(
+ val context: Id,
+ val focus: Id,
+ val range: IntRange
+ )
+
+ /**
+ * Response for the use-case.
+ * @param cursor caret position
+ * @param blocks ids of the new blocks
+ * @param payload response payload
+ */
+ data class Response(
+ val cursor: Int,
+ val blocks: List,
+ val payload: Payload
+ )
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt b/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt
index 6f4ab8a540..3369d3aa7c 100644
--- a/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt
+++ b/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt
@@ -25,7 +25,6 @@ fun Map>.asRender(anchor: String): List {
children.forEach { child ->
when (child.content) {
is Content.Text,
- is Content.Image,
is Content.Link,
is Content.Divider,
is Content.Bookmark,
diff --git a/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt b/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt
index c78d47273a..1afd5c3810 100644
--- a/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt
+++ b/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt
@@ -683,12 +683,11 @@ class BlockExtensionTest {
@Test(expected = ClassCastException::class)
fun `should throw exception when block is not text`() {
+
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
- content = Block.Content.Dashboard(
- type = Block.Content.Dashboard.Type.MAIN_SCREEN
- ),
+ content = Block.Content.Divider,
children = emptyList()
)
val range = IntRange(10, 13)
diff --git a/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java b/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java
deleted file mode 100644
index 56c473c36a..0000000000
--- a/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.agileburo.anytype;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("com.agileburo.anytype.test", appContext.getPackageName());
- }
-}
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt
index 4076d1ec32..92c4ced6c8 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt
@@ -6,8 +6,8 @@ import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.data.auth.model.WalletEntity
import com.agileburo.anytype.data.auth.repo.AuthRemote
import com.agileburo.anytype.middleware.EventProxy
+import com.agileburo.anytype.middleware.converters.toAccountEntity
import com.agileburo.anytype.middleware.interactor.Middleware
-import com.agileburo.anytype.middleware.toAccountEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt
index 7449639783..b307c40973 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt
@@ -5,8 +5,8 @@ import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.data.auth.model.Response
import com.agileburo.anytype.data.auth.repo.block.BlockRemote
+import com.agileburo.anytype.middleware.converters.mark
import com.agileburo.anytype.middleware.interactor.Middleware
-import com.agileburo.anytype.middleware.toMiddleware
class BlockMiddleware(
private val middleware: Middleware
@@ -41,7 +41,7 @@ class BlockMiddleware(
command.contextId,
command.blockId,
command.text,
- command.marks.map { it.toMiddleware() }
+ command.marks.map { it.mark() }
)
}
@@ -133,4 +133,8 @@ class BlockMiddleware(
override suspend fun paste(
command: CommandEntity.Paste
): Response.Clipboard.Paste = middleware.paste(command)
+
+ override suspend fun copy(
+ command: CommandEntity.Copy
+ ): Response.Clipboard.Copy = middleware.copy(command)
}
\ No newline at end of file
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt
new file mode 100644
index 0000000000..a50fe80cd6
--- /dev/null
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt
@@ -0,0 +1,18 @@
+package com.agileburo.anytype.middleware.converters
+
+import anytype.clipboard.ClipboardOuterClass.Clipboard
+import com.agileburo.anytype.data.auth.mapper.Serializer
+import com.agileburo.anytype.data.auth.model.BlockEntity
+
+class ClipboardSerializer : Serializer {
+
+ override fun serialize(blocks: List): ByteArray {
+ val models = blocks.map { it.block() }
+ val clipboard = Clipboard.newBuilder().addAllBlocks(models).build()
+ return clipboard.toByteArray()
+ }
+
+ override fun deserialize(blob: ByteArray): List {
+ return Clipboard.parseFrom(blob).blocksList.blocks()
+ }
+}
\ No newline at end of file
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt
similarity index 85%
rename from middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt
rename to middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt
index 7e2f73220c..4d988d9f92 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt
@@ -1,7 +1,6 @@
-package com.agileburo.anytype.middleware
+package com.agileburo.anytype.middleware.converters
import anytype.Events
-import anytype.model.Models
import anytype.model.Models.Account
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.AccountEntity
@@ -11,7 +10,6 @@ import com.google.protobuf.Struct
import com.google.protobuf.Value
import timber.log.Timber
-
fun Events.Event.Account.Show.toAccountEntity(): AccountEntity {
return AccountEntity(
id = account.id,
@@ -22,69 +20,6 @@ fun Events.Event.Account.Show.toAccountEntity(): AccountEntity {
)
}
-fun BlockEntity.Content.Text.Mark.toMiddleware(): Block.Content.Text.Mark {
- val rangeModel = Models.Range.newBuilder()
- .setFrom(range.first)
- .setTo(range.last)
- .build()
-
- return when (type) {
- BlockEntity.Content.Text.Mark.Type.BOLD -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.Bold)
- .setRange(rangeModel)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.ITALIC -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.Italic)
- .setRange(rangeModel)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.Strikethrough)
- .setRange(rangeModel)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.TEXT_COLOR -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.TextColor)
- .setRange(rangeModel)
- .setParam(param)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.LINK -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.Link)
- .setRange(rangeModel)
- .setParam(param)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.BackgroundColor)
- .setRange(rangeModel)
- .setParam(param)
- .build()
- }
- BlockEntity.Content.Text.Mark.Type.KEYBOARD -> {
- Block.Content.Text.Mark
- .newBuilder()
- .setType(Block.Content.Text.Mark.Type.Keyboard)
- .setRange(rangeModel)
- .build()
- }
- else -> throw IllegalStateException("Unsupported mark type: ${type.name}")
- }
-}
-
fun Block.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
fields.fieldsMap.forEach { (key, value) ->
result.map[key] = when (val case = value.kindCase) {
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt
new file mode 100644
index 0000000000..3cdcba31b7
--- /dev/null
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt
@@ -0,0 +1,234 @@
+package com.agileburo.anytype.middleware.converters
+
+import anytype.model.Models.Block
+import anytype.model.Models.Range
+import com.agileburo.anytype.data.auth.model.BlockEntity
+import com.google.protobuf.Struct
+import com.google.protobuf.Value
+
+typealias Mark = Block.Content.Text.Mark
+typealias File = Block.Content.File
+typealias FileState = Block.Content.File.State
+typealias FileType = Block.Content.File.Type
+typealias Link = Block.Content.Link
+typealias LinkType = Block.Content.Link.Style
+typealias Bookmark = Block.Content.Bookmark
+typealias Marks = Block.Content.Text.Marks
+typealias Text = Block.Content.Text
+typealias Layout = Block.Content.Layout
+typealias LayoutStyle = Block.Content.Layout.Style
+typealias Divider = Block.Content.Div
+typealias DividerStyle = Block.Content.Div.Style
+
+//region block mapping
+
+fun BlockEntity.block(): Block {
+
+ val builder = Block.newBuilder()
+
+ builder.id = id
+
+ when (val content = content) {
+ is BlockEntity.Content.Text -> {
+ builder.text = content.text()
+ }
+ is BlockEntity.Content.Bookmark -> {
+ builder.bookmark = content.bookmark()
+ }
+ is BlockEntity.Content.File -> {
+ builder.file = content.file()
+ }
+ is BlockEntity.Content.Link -> {
+ builder.link = content.link()
+ }
+ is BlockEntity.Content.Layout -> {
+ builder.layout = content.layout()
+ }
+ is BlockEntity.Content.Divider -> {
+ builder.div = content.divider()
+ }
+ }
+
+ return builder.build()
+}
+
+//endregion
+
+//region text block mapping
+
+fun BlockEntity.Content.Text.text(): Text {
+ return Text
+ .newBuilder()
+ .setText(text)
+ .setMarks(marks())
+ .setStyle(style.toMiddleware())
+ .build()
+}
+
+fun BlockEntity.Content.Text.marks(): Marks {
+ return Marks
+ .newBuilder()
+ .addAllMarks(marks.map { it.mark() })
+ .build()
+}
+
+fun BlockEntity.Content.Text.Mark.mark(): Mark = when (type) {
+ BlockEntity.Content.Text.Mark.Type.BOLD -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.Bold)
+ .setRange(range.range())
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.ITALIC -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.Italic)
+ .setRange(range.range())
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.Strikethrough)
+ .setRange(range.range())
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.TEXT_COLOR -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.TextColor)
+ .setRange(range.range())
+ .setParam(param)
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.LINK -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.Link)
+ .setRange(range.range())
+ .setParam(param)
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.BackgroundColor)
+ .setRange(range.range())
+ .setParam(param)
+ .build()
+ }
+ BlockEntity.Content.Text.Mark.Type.KEYBOARD -> {
+ Mark.newBuilder()
+ .setType(Block.Content.Text.Mark.Type.Keyboard)
+ .setRange(range.range())
+ .build()
+ }
+ else -> throw IllegalStateException("Unsupported mark type: ${type.name}")
+}
+
+//endregion
+
+//region bookmark block mapping
+
+fun BlockEntity.Content.Bookmark.bookmark(): Bookmark {
+ val builder = Bookmark.newBuilder()
+ description?.let { builder.setDescription(it) }
+ favicon?.let { builder.setFaviconHash(it) }
+ title?.let { builder.setTitle(it) }
+ url?.let { builder.setUrl(it) }
+ image?.let { builder.setImageHash(it) }
+ return builder.build()
+}
+
+//endregion
+
+//region file block mapping
+
+fun BlockEntity.Content.File.file(): File {
+ val builder = File.newBuilder()
+ hash?.let { builder.setHash(it) }
+ name?.let { builder.setName(it) }
+ mime?.let { builder.setMime(it) }
+ size?.let { builder.setSize(it) }
+ state?.let { builder.setState(it.state()) }
+ type?.let { builder.setType(it.type()) }
+ return builder.build()
+}
+
+fun BlockEntity.Content.File.State.state(): FileState = when (this) {
+ BlockEntity.Content.File.State.EMPTY -> FileState.Empty
+ BlockEntity.Content.File.State.UPLOADING -> FileState.Uploading
+ BlockEntity.Content.File.State.DONE -> FileState.Done
+ BlockEntity.Content.File.State.ERROR -> FileState.Error
+}
+
+fun BlockEntity.Content.File.Type.type(): FileType = when (this) {
+ BlockEntity.Content.File.Type.NONE -> FileType.None
+ BlockEntity.Content.File.Type.FILE -> FileType.File
+ BlockEntity.Content.File.Type.IMAGE -> FileType.Image
+ BlockEntity.Content.File.Type.VIDEO -> FileType.Video
+}
+
+//endregion
+
+//region link mapping
+
+fun BlockEntity.Content.Link.link(): Link {
+ return Link.newBuilder()
+ .setTargetBlockId(target)
+ .setStyle(type.type())
+ .setFields(fields.fields())
+ .build()
+}
+
+fun BlockEntity.Content.Link.Type.type() : LinkType = when(this) {
+ BlockEntity.Content.Link.Type.ARCHIVE -> LinkType.Archive
+ BlockEntity.Content.Link.Type.DASHBOARD -> LinkType.Dashboard
+ BlockEntity.Content.Link.Type.DATA_VIEW -> LinkType.Dataview
+ BlockEntity.Content.Link.Type.PAGE -> LinkType.Page
+}
+
+//endregion
+
+//region layout mapping
+
+fun BlockEntity.Content.Layout.layout() : Layout {
+ val builder = Layout.newBuilder()
+ when(type) {
+ BlockEntity.Content.Layout.Type.ROW -> builder.style = LayoutStyle.Row
+ BlockEntity.Content.Layout.Type.COLUMN -> builder.style = LayoutStyle.Column
+ BlockEntity.Content.Layout.Type.DIV -> builder.style = LayoutStyle.Div
+ }
+ return builder.build()
+}
+
+//endregion
+
+//region divider mapping
+
+fun BlockEntity.Content.Divider.divider() : Divider {
+ return Divider.newBuilder().setStyle(DividerStyle.Line).build()
+}
+
+//endregion
+
+//region other mapping
+
+fun BlockEntity.Fields.fields() : Struct {
+ val builder = Struct.newBuilder()
+ map.forEach { (key, value) ->
+ if (key != null && value != null)
+ when(value) {
+ is String -> {
+ builder.putFields(key, Value.newBuilder().setStringValue(value).build())
+ }
+ is Boolean -> {
+ builder.putFields(key, Value.newBuilder().setBoolValue(value).build())
+ }
+ is Double-> {
+ builder.putFields(key, Value.newBuilder().setNumberValue(value).build())
+ }
+ else -> throw IllegalStateException("Unexpected value type: ${value::class.java}")
+ }
+ }
+ return builder.build()
+}
+
+fun IntRange.range(): Range = Range.newBuilder().setFrom(first).setTo(last).build()
+
+//endregion
\ No newline at end of file
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java
index b3fffe8ba4..daabbd5a11 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java
@@ -782,6 +782,8 @@ public class Middleware {
html = command.getHtml();
}
+ List blocks = mapper.toMiddleware(command.getBlocks());
+
Block.Paste.Request request = Block.Paste.Request
.newBuilder()
.setContextId(command.getContext())
@@ -789,6 +791,7 @@ public class Middleware {
.setTextSlot(command.getText())
.setHtmlSlot(html)
.setSelectedTextRange(range)
+ .addAllAnySlot(blocks)
.addAllSelectedBlockIds(command.getSelected())
.build();
@@ -808,4 +811,47 @@ public class Middleware {
mapper.toPayload(response.getEvent())
);
}
+
+ public Response.Clipboard.Copy copy(CommandEntity.Copy command) throws Exception {
+
+ Range range;
+
+ if (command.getRange() != null) {
+ range = Range.newBuilder()
+ .setFrom(command.getRange().getFirst())
+ .setTo(command.getRange().getLast())
+ .build();
+ } else {
+ range = Range.getDefaultInstance();
+ }
+
+ List blocks = mapper.toMiddleware(command.getBlocks());
+
+ Block.Copy.Request.Builder builder = Block.Copy.Request.newBuilder();
+
+ if (range != null) {
+ builder.setSelectedTextRange(range);
+ }
+
+ Block.Copy.Request request = builder
+ .setContextId(command.getContext())
+ .addAllBlocks(blocks)
+ .build();
+
+ if (BuildConfig.DEBUG) {
+ Timber.d(request.getClass().getName() + "\n" + request.toString());
+ }
+
+ Block.Copy.Response response = service.blockCopy(request);
+
+ if (BuildConfig.DEBUG) {
+ Timber.d(response.getClass().getName() + "\n" + response.toString());
+ }
+
+ return new Response.Clipboard.Copy(
+ response.getTextSlot(),
+ response.getHtmlSlot(),
+ mapper.toEntity(response.getAnySlotList())
+ );
+ }
}
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt
index 2312ec0499..0dca78947b 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt
@@ -3,10 +3,10 @@ package com.agileburo.anytype.middleware.interactor
import anytype.Events.Event
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.EventEntity
-import com.agileburo.anytype.middleware.blocks
-import com.agileburo.anytype.middleware.entity
-import com.agileburo.anytype.middleware.fields
-import com.agileburo.anytype.middleware.marks
+import com.agileburo.anytype.middleware.converters.blocks
+import com.agileburo.anytype.middleware.converters.entity
+import com.agileburo.anytype.middleware.converters.fields
+import com.agileburo.anytype.middleware.converters.marks
fun Event.Message.toEntity(
context: String
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt
index 46062d7ef9..685d0ef123 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt
@@ -2,7 +2,9 @@ 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
+import com.agileburo.anytype.middleware.converters.state
+import com.agileburo.anytype.middleware.converters.toMiddleware
+import com.agileburo.anytype.middleware.converters.type
class MiddlewareFactory {
@@ -29,8 +31,8 @@ class MiddlewareFactory {
}
is BlockEntity.Prototype.File -> {
val file = Block.Content.File.newBuilder().apply {
- state = prototype.state.toMiddleware()
- type = prototype.type.toMiddleware()
+ state = prototype.state.state()
+ type = prototype.type.type()
}
builder.setFile(file).build()
}
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt
index 088788e1fa..e8461dd822 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt
@@ -5,10 +5,16 @@ import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.data.auth.model.PositionEntity
-import com.agileburo.anytype.middleware.toMiddleware
+import com.agileburo.anytype.middleware.converters.block
+import com.agileburo.anytype.middleware.converters.blocks
+import com.agileburo.anytype.middleware.converters.toMiddleware
class MiddlewareMapper {
+ fun toMiddleware(blocks: List) : List {
+ return blocks.map { it.block() }
+ }
+
fun toMiddleware(style: BlockEntity.Content.Text.Style): Block.Content.Text.Style {
return style.toMiddleware()
}
@@ -30,4 +36,8 @@ class MiddlewareMapper {
fun toMiddleware(alignment: BlockEntity.Align): Block.Align {
return alignment.toMiddleware()
}
+
+ fun toEntity(blocks: List) : List {
+ return blocks.blocks()
+ }
}
\ No newline at end of file
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java
index 47edb9a33f..20ded0845e 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java
@@ -327,4 +327,15 @@ public class DefaultMiddlewareService implements MiddlewareService {
return response;
}
}
+
+ @Override
+ public Block.Copy.Response blockCopy(Block.Copy.Request request) throws Exception {
+ byte[] encoded = Lib.blockCopy(request.toByteArray());
+ Block.Copy.Response response = Block.Copy.Response.parseFrom(encoded);
+ if (response.getError() != null && response.getError().getCode() != Block.Copy.Response.Error.Code.NULL) {
+ throw new Exception(response.getError().getDescription());
+ } else {
+ return response;
+ }
+ }
}
diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java
index 7532c71806..6a85b0f4d9 100644
--- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java
+++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java
@@ -67,4 +67,6 @@ public interface MiddlewareService {
Block.Set.Details.Response blockSetDetails(Block.Set.Details.Request request) throws Exception;
Block.Paste.Response blockPaste(Block.Paste.Request request) throws Exception;
+
+ Block.Copy.Response blockCopy(Block.Copy.Request request) throws Exception;
}
diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt
index 18d6b172a1..85c91345ef 100644
--- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt
+++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt
@@ -1117,6 +1117,20 @@ class PageViewModel(
}
}
+ fun onMultiSelectCopyClicked() {
+ viewModelScope.launch {
+ orchestrator.proxies.intents.send(
+ Intent.Clipboard.Copy(
+ context = context,
+ blocks = blocks.filter { block ->
+ currentSelection().contains(block.id)
+ },
+ range = null
+ )
+ )
+ }
+ }
+
fun onMultiSelectModeSelectAllClicked() {
(stateData.value as ViewState.Success).let { state ->
val update = state.blocks.map { block ->
@@ -1436,8 +1450,6 @@ class PageViewModel(
}
fun onPaste(
- plain: String,
- html: String?,
range: IntRange
) {
viewModelScope.launch {
@@ -1446,10 +1458,21 @@ class PageViewModel(
context = context,
focus = orchestrator.stores.focus.current(),
range = range,
- blocks = emptyList(),
- selected = emptyList(),
- html = html,
- text = plain
+ selected = emptyList()
+ )
+ )
+ }
+ }
+
+ fun onCopy(
+ range: IntRange
+ ) {
+ viewModelScope.launch {
+ orchestrator.proxies.intents.send(
+ Intent.Clipboard.Copy(
+ context = context,
+ range = range,
+ blocks = listOf(blocks.first { it.id == focus.value })
)
)
}
diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt
index 381b99b0fb..d0301d3cff 100644
--- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt
+++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt
@@ -56,9 +56,11 @@ sealed class Intent {
val context: Id,
val focus: Id,
val selected: List,
- val range: IntRange,
- val text: String,
- val html: String?,
+ val range: IntRange
+ ) : Clipboard()
+ class Copy(
+ val context: Id,
+ val range: IntRange?,
val blocks: List
) : Clipboard()
}
diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt
index 8ccb628751..024d57d41c 100644
--- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt
+++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt
@@ -1,6 +1,8 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.domain.block.interactor.*
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.model.Payload
@@ -28,7 +30,8 @@ class Orchestrator(
private val updateText: UpdateText,
private val updateAlignment: UpdateAlignment,
private val setupBookmark: SetupBookmark,
- private val paste: Clipboard.Paste,
+ private val copy: Copy,
+ private val paste: Paste,
private val undo: Undo,
private val redo: Redo,
val memory: Editor.Memory,
@@ -263,14 +266,10 @@ class Orchestrator(
}
is Intent.Clipboard.Paste -> {
paste(
- params = Clipboard.Paste.Params(
+ params = Paste.Params(
context = intent.context,
focus = intent.focus,
- range = intent.range,
- blocks = emptyList(),
- html = intent.html,
- text = intent.text,
- selected = intent.selected
+ range = intent.range
)
).proceed(
failure = defaultOnError,
@@ -280,6 +279,20 @@ class Orchestrator(
}
)
}
+ is Intent.Clipboard.Copy -> {
+ copy(
+ params = Copy.Params(
+ context = intent.context,
+ blocks = intent.blocks,
+ range = intent.range
+ )
+ ).proceed(
+ success = {
+ Timber.d("Copy sucessful")
+ },
+ failure = defaultOnError
+ )
+ }
}
}
}
diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt
index 8aa9b0fe16..b338fcd0af 100644
--- a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt
+++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt
@@ -11,6 +11,8 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Position
+import com.agileburo.anytype.domain.clipboard.Copy
+import com.agileburo.anytype.domain.clipboard.Paste
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.download.DownloadFile
@@ -113,7 +115,10 @@ class PageViewModelTest {
lateinit var uploadUrl: UploadUrl
@Mock
- lateinit var paste: Clipboard.Paste
+ lateinit var paste: Paste
+
+ @Mock
+ lateinit var copy: Copy
@Mock
lateinit var undo: Undo
@@ -4266,7 +4271,8 @@ class PageViewModelTest {
),
updateAlignment = updateAlignment,
setupBookmark = setupBookmark,
- paste = paste
+ paste = paste,
+ copy = copy
)
)
}
diff --git a/protobuf/src/main/proto/clipboard.proto b/protobuf/src/main/proto/clipboard.proto
new file mode 100644
index 0000000000..9f0f73cda3
--- /dev/null
+++ b/protobuf/src/main/proto/clipboard.proto
@@ -0,0 +1,8 @@
+syntax = "proto3";
+package anytype.clipboard;
+
+import "models.proto";
+
+message Clipboard {
+ repeated anytype.model.Block blocks = 1;
+}
\ No newline at end of file
diff --git a/sample/build.gradle b/sample/build.gradle
index 8979feaac8..abd3931b16 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -42,6 +42,10 @@ dependencies {
def applicationDependencies = rootProject.ext.mainApplication
+ def protobufDependencies = rootProject.ext.protobuf
+
+ implementation protobufDependencies.protobufJava
+
implementation 'com.github.HBiSoft:PickiT:0.1.9'
implementation 'com.vdurmont:emoji-java:5.1.1'
@@ -49,6 +53,8 @@ dependencies {
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-page-icon-picker-widget')
+ implementation project(':middleware')
+ implementation project(':protobuf')
implementation applicationDependencies.timber
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index a0be0706e1..cddebdbdf7 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -18,13 +18,17 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
-
-
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt b/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt
new file mode 100644
index 0000000000..4fac563072
--- /dev/null
+++ b/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt
@@ -0,0 +1,85 @@
+package com.agileburo.anytype.sample
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.net.Uri
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import anytype.clipboard.ClipboardOuterClass.Clipboard
+import anytype.model.Models.Block
+import com.agileburo.anytype.core_ui.common.ThemeColor
+import kotlinx.android.synthetic.main.activity_clipboard.*
+
+class ClipboardActivity : AppCompatActivity() {
+
+ private val cm: ClipboardManager
+ get() = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_clipboard)
+ setup()
+ }
+
+ private fun setup() {
+ write.setOnClickListener { write() }
+ read.setOnClickListener { read() }
+ copy.setOnClickListener { copy() }
+ paste.setOnClickListener { paste() }
+ }
+
+ private fun write() {
+
+ val text = Block.Content.Text
+ .newBuilder()
+ .setText("Everything was in confusion")
+ .setColor(ThemeColor.ICE.title)
+ .setStyle(Block.Content.Text.Style.Checkbox)
+ .build()
+
+ val blocks = listOf(
+ Block.newBuilder()
+ .setId("1")
+ .setText(text)
+ .build(),
+ Block.newBuilder()
+ .setId("2")
+ .setText(text)
+ .build()
+ )
+
+ val clipboard = Clipboard.newBuilder().addAllBlocks(blocks).build()
+
+ val stream = openFileOutput(DEFAULT_FILE_NAME, Context.MODE_PRIVATE)
+
+ clipboard.writeTo(stream)
+
+ stream.flush()
+
+ stream.close()
+ }
+
+ private fun read() {
+ val stream = openFileInput(DEFAULT_FILE_NAME)
+ val board = Clipboard.parseFrom(stream)
+ output.text = board.toString()
+ }
+
+ private fun copy() {
+ output.clearComposingText()
+ val uri = Uri.parse(BASE_URI)
+ val clip = ClipData.newUri(contentResolver, "URI", uri)
+ cm.setPrimaryClip(clip)
+ }
+
+ private fun paste() {
+ output.text = cm.primaryClip.toString()
+ }
+
+ companion object {
+ private const val DEFAULT_FILE_NAME = "test"
+ private const val AUTHORITY = "com.agileburo.anytype.sample"
+ private const val BASE_URI = "content://$AUTHORITY"
+ }
+}
diff --git a/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt b/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt
index f144897117..1e241d67b4 100644
--- a/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt
+++ b/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt
@@ -18,4 +18,8 @@ class SampleApp : Application() {
else
Timber.plant(CrashlyticsTree())
}
+
+ companion object {
+ const val BASE_URI = "content://com.agileburo.anytype"
+ }
}
\ No newline at end of file
diff --git a/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt b/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt
new file mode 100644
index 0000000000..b49939aa71
--- /dev/null
+++ b/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt
@@ -0,0 +1,64 @@
+package com.agileburo.anytype.sample.helpers
+
+import java.util.*
+import java.util.concurrent.ThreadLocalRandom
+
+object MockDataFactory {
+
+ fun randomUuid(): String {
+ return UUID.randomUUID().toString()
+ }
+
+ fun randomString(): String {
+ return randomUuid()
+ }
+
+
+ fun randomInt(): Int {
+ return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
+ }
+
+ fun randomInt(max: Int): Int {
+ return ThreadLocalRandom.current().nextInt(0, max)
+ }
+
+ fun randomLong(): Long {
+ return randomInt().toLong()
+ }
+
+ fun randomFloat(): Float {
+ return randomInt().toFloat()
+ }
+
+ fun randomDouble(): Double {
+ return randomInt().toDouble()
+ }
+
+ fun randomBoolean(): Boolean {
+ return Math.random() < 0.5
+ }
+
+ fun makeIntList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomInt())
+ }
+ return items
+ }
+
+ fun makeStringList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomUuid())
+ }
+ return items
+ }
+
+ fun makeDoubleList(count: Int): List {
+ val items = mutableListOf()
+ repeat(count) {
+ items.add(randomDouble())
+ }
+ return items
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/res/layout/activity_clipboard.xml b/sample/src/main/res/layout/activity_clipboard.xml
new file mode 100644
index 0000000000..684bc1c26f
--- /dev/null
+++ b/sample/src/main/res/layout/activity_clipboard.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 4d7e590acb..34e23e620a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,3 +13,4 @@ include ':app',
':library-page-icon-picker-widget',
':library-emojifier',
':sample'
+include ':clipboard'