Feature/copy paste inside anytype (#473)
This commit is contained in:
parent
267e5e1b0b
commit
98367451ff
|
@ -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 ⚙️
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -13,7 +13,8 @@ import javax.inject.Singleton
|
|||
ConfigModule::class,
|
||||
DeviceModule::class,
|
||||
UtilModule::class,
|
||||
EmojiModule::class
|
||||
EmojiModule::class,
|
||||
ClipboardModule::class
|
||||
]
|
||||
)
|
||||
interface MainComponent {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
app:layout_behavior="@string/bottom_sheet_behavior">
|
||||
|
||||
<FrameLayout
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:id="@+id/select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
|
|
1
clipboard/.gitignore
vendored
Normal file
1
clipboard/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
62
clipboard/build.gradle
Normal file
62
clipboard/build.gradle
Normal file
|
@ -0,0 +1,62 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
def config = rootProject.extensions.getByName("ext")
|
||||
|
||||
compileSdkVersion config["compile_sdk"]
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion config["min_sdk"]
|
||||
targetSdkVersion config["target_sdk"]
|
||||
versionCode config["version_code"]
|
||||
versionName config["version_name"]
|
||||
|
||||
testInstrumentationRunner config["test_runner"]
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField ANYTYPE_CLIPBOARD_URI_TYPE, ANYTYPE_CLIPBOARD_URI, ANYTYPE_CLIPBOARD_URI_VALUE
|
||||
buildConfigField ANYTYPE_CLIPBOARD_LABEL_TYPE, ANYTYPE_CLIPBOARD_LABEL, ANYTYPE_CLIPBOARD_LABEL_VALUE
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
buildConfigField ANYTYPE_CLIPBOARD_URI_TYPE, ANYTYPE_CLIPBOARD_URI, ANYTYPE_CLIPBOARD_URI_VALUE
|
||||
buildConfigField ANYTYPE_CLIPBOARD_LABEL_TYPE, ANYTYPE_CLIPBOARD_LABEL, ANYTYPE_CLIPBOARD_LABEL_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def applicationDependencies = rootProject.ext.mainApplication
|
||||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
|
||||
implementation project(':data')
|
||||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.timber
|
||||
|
||||
testImplementation unitTestDependencies.junit
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.androidXTestCore
|
||||
testImplementation unitTestDependencies.robolectric
|
||||
}
|
0
clipboard/consumer-rules.pro
Normal file
0
clipboard/consumer-rules.pro
Normal file
6
clipboard/gradle.properties
Normal file
6
clipboard/gradle.properties
Normal file
|
@ -0,0 +1,6 @@
|
|||
ANYTYPE_CLIPBOARD_LABEL=ANYTYPE_CLIPBOARD_LABEL
|
||||
ANYTYPE_CLIPBOARD_LABEL_VALUE="Anytype"
|
||||
ANYTYPE_CLIPBOARD_LABEL_TYPE=String
|
||||
ANYTYPE_CLIPBOARD_URI=ANYTYPE_CLIPBOARD_URI
|
||||
ANYTYPE_CLIPBOARD_URI_VALUE="content://com.agileburo.anytype"
|
||||
ANYTYPE_CLIPBOARD_URI_TYPE=String
|
21
clipboard/proguard-rules.pro
vendored
Normal file
21
clipboard/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
1
clipboard/src/main/AndroidManifest.xml
Normal file
1
clipboard/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1 @@
|
|||
<manifest package="com.agileburo.anytype.clipboard" />
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<BlockEntity>) {
|
||||
val serialized = serializer.serialize(blocks)
|
||||
context.openFileOutput(CLIPBOARD_FILE_NAME, Context.MODE_PRIVATE).use {
|
||||
it.write(serialized)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetch(): List<BlockEntity> {
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<Context>()
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<Int> {
|
||||
val items = mutableListOf<Int>()
|
||||
repeat(count) {
|
||||
items.add(randomInt())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun makeStringList(count: Int): List<String> {
|
||||
val items = mutableListOf<String>()
|
||||
repeat(count) {
|
||||
items.add(randomUuid())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun makeDoubleList(count: Int): List<Double> {
|
||||
val items = mutableListOf<Double>()
|
||||
repeat(count) {
|
||||
items.add(randomDouble())
|
||||
}
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -62,4 +62,5 @@ dependencies {
|
|||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.robolectric
|
||||
testImplementation unitTestDependencies.androidXTestCore
|
||||
testImplementation unitTestDependencies.mockitoKotlin
|
||||
}
|
|
@ -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<BlockViewHolder>() {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<TextWatcher> = 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?) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<FrameLayout
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:id="@+id/delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -30,6 +30,7 @@
|
|||
android:id="@+id/convert"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -52,6 +53,7 @@
|
|||
style="@style/BottomMultiSelectToolbarOptionStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -63,7 +65,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/more" />
|
||||
android:text="@string/copy" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -88,12 +90,12 @@
|
|||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:visibility="invisible"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
|
|
@ -197,5 +197,6 @@
|
|||
|
||||
<string name="undo">Undo</string>
|
||||
<string name="redo">Redo</string>
|
||||
<string name="copy">Copy</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
)
|
|
@ -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<BlockEntity>) : ByteArray
|
||||
fun deserialize(blob: ByteArray) : List<BlockEntity>
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -133,4 +133,10 @@ class CommandEntity {
|
|||
val html: String?,
|
||||
val blocks: List<BlockEntity>
|
||||
)
|
||||
|
||||
data class Copy(
|
||||
val context: String,
|
||||
val range: IntRange?,
|
||||
val blocks: List<BlockEntity>
|
||||
)
|
||||
}
|
|
@ -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<String>,
|
||||
val payload: PayloadEntity
|
||||
) : Clipboard()
|
||||
|
||||
class Copy(
|
||||
val plain: String,
|
||||
val html: String?,
|
||||
val blocks: List<BlockEntity>
|
||||
) : Clipboard()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.agileburo.anytype.data.auth.other
|
||||
|
||||
interface ClipboardUriMatcher {
|
||||
fun isAnytypeUri(uri: String) : Boolean
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<Block>) {
|
||||
factory.storage.persist(
|
||||
blocks = blocks.map { it.toEntity() }
|
||||
)
|
||||
factory.system.put(
|
||||
text = text,
|
||||
html = html
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun blocks(): List<Block> {
|
||||
return factory.storage.fetch().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun clip(): Clip? = factory.system.clip()
|
||||
}
|
|
@ -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<BlockEntity>)
|
||||
fun fetch() : List<BlockEntity>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
}
|
|
@ -53,4 +53,6 @@ dependencies {
|
|||
|
||||
testImplementation unitTestDependencies.junit
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.androidXTestCore
|
||||
testImplementation unitTestDependencies.robolectric
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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<Paste.Response, Paste.Params>(), 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<Id>,
|
||||
val range: IntRange,
|
||||
val text: String,
|
||||
val html: String?,
|
||||
val blocks: List<Block>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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<Id>,
|
||||
val payload: Payload
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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<Block>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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<Block>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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<Block>)
|
||||
|
||||
/**
|
||||
* @return Anytype blocks currently stored on (or linked to) the clipboard
|
||||
*/
|
||||
suspend fun blocks() : List<Block>
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
}
|
|
@ -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<Unit, Copy.Params>() {
|
||||
|
||||
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<Block>
|
||||
)
|
||||
|
||||
/**
|
||||
* @param text plain text
|
||||
* @param html optional html
|
||||
* @param blocks anytype clipboard slot
|
||||
*/
|
||||
class Response(
|
||||
val text: String,
|
||||
val html: String?,
|
||||
val blocks: List<Block>
|
||||
)
|
||||
}
|
|
@ -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<Paste.Response, Paste.Params>() {
|
||||
|
||||
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<Id>,
|
||||
val payload: Payload
|
||||
)
|
||||
}
|
|
@ -25,7 +25,6 @@ fun Map<String, List<Block>>.asRender(anchor: String): List<Block> {
|
|||
children.forEach { child ->
|
||||
when (child.content) {
|
||||
is Content.Text,
|
||||
is Content.Image,
|
||||
is Content.Link,
|
||||
is Content.Divider,
|
||||
is Content.Bookmark,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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<BlockEntity>): ByteArray {
|
||||
val models = blocks.map { it.block() }
|
||||
val clipboard = Clipboard.newBuilder().addAllBlocks(models).build()
|
||||
return clipboard.toByteArray()
|
||||
}
|
||||
|
||||
override fun deserialize(blob: ByteArray): List<BlockEntity> {
|
||||
return Clipboard.parseFrom(blob).blocksList.blocks()
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
|
@ -782,6 +782,8 @@ public class Middleware {
|
|||
html = command.getHtml();
|
||||
}
|
||||
|
||||
List<Models.Block> 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<Models.Block> 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<BlockEntity>) : List<Block> {
|
||||
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<Block>) : List<BlockEntity> {
|
||||
return blocks.blocks()
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -56,9 +56,11 @@ sealed class Intent {
|
|||
val context: Id,
|
||||
val focus: Id,
|
||||
val selected: List<Id>,
|
||||
val range: IntRange,
|
||||
val text: String,
|
||||
val html: String?,
|
||||
val range: IntRange
|
||||
) : Clipboard()
|
||||
class Copy(
|
||||
val context: Id,
|
||||
val range: IntRange?,
|
||||
val blocks: List<Block>
|
||||
) : Clipboard()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
8
protobuf/src/main/proto/clipboard.proto
Normal file
8
protobuf/src/main/proto/clipboard.proto
Normal file
|
@ -0,0 +1,8 @@
|
|||
syntax = "proto3";
|
||||
package anytype.clipboard;
|
||||
|
||||
import "models.proto";
|
||||
|
||||
message Clipboard {
|
||||
repeated anytype.model.Block blocks = 1;
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -18,13 +18,17 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name=".MainActivity" />
|
||||
<activity android:name=".KeyboardActivity" android:windowSoftInputMode="adjustResize">
|
||||
<activity android:name=".ClipboardActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".MainActivity" />
|
||||
<activity
|
||||
android:name=".KeyboardActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -18,4 +18,8 @@ class SampleApp : Application() {
|
|||
else
|
||||
Timber.plant(CrashlyticsTree())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URI = "content://com.agileburo.anytype"
|
||||
}
|
||||
}
|
|
@ -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<Int> {
|
||||
val items = mutableListOf<Int>()
|
||||
repeat(count) {
|
||||
items.add(randomInt())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun makeStringList(count: Int): List<String> {
|
||||
val items = mutableListOf<String>()
|
||||
repeat(count) {
|
||||
items.add(randomUuid())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun makeDoubleList(count: Int): List<Double> {
|
||||
val items = mutableListOf<Double>()
|
||||
repeat(count) {
|
||||
items.add(randomDouble())
|
||||
}
|
||||
return items
|
||||
}
|
||||
}
|
69
sample/src/main/res/layout/activity_clipboard.xml
Normal file
69
sample/src/main/res/layout/activity_clipboard.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<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="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="copy"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.07999998" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/paste"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="paste"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/copy" />
|
||||
|
||||
<TextView
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
tools:text="sdsdsdsd"
|
||||
android:textColor="@color/white"
|
||||
android:background="@color/black"
|
||||
android:fontFamily="monospace"
|
||||
android:id="@+id/output"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/paste" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/write"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="write"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/read"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="read"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -13,3 +13,4 @@ include ':app',
|
|||
':library-page-icon-picker-widget',
|
||||
':library-emojifier',
|
||||
':sample'
|
||||
include ':clipboard'
|
||||
|
|
Loading…
Reference in New Issue
Block a user