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

DROID-2550 Sharing extension | FileDrop command implementation (#1362)

This commit is contained in:
Konstantin Ivanov 2024-07-06 18:22:50 +02:00 committed by GitHub
parent 986155f10a
commit 098a6e7146
Signed by: github
GPG key ID: B5690EEEBB952194
15 changed files with 609 additions and 153 deletions

View file

@ -3,6 +3,8 @@ package com.anytypeio.anytype.di.feature.sharing
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerDialog
import com.anytypeio.anytype.data.auth.event.EventProcessDropFilesDateChannel
import com.anytypeio.anytype.data.auth.event.EventProcessDropFilesRemoteChannel
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
@ -13,13 +15,17 @@ import com.anytypeio.anytype.domain.device.FileSharer
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.workspace.EventProcessDropFilesChannel
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.middleware.EventProxy
import com.anytypeio.anytype.middleware.interactor.EventProcessDropFilesMiddlewareChannel
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.sharing.AddToAnytypeViewModel
import com.anytypeio.anytype.ui.sharing.SharingFragment
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
@Component(
dependencies = [AddToAnytypeDependencies::class],
@ -40,6 +46,19 @@ interface AddToAnytypeComponent {
@Module
object AddToAnytypeModule {
@Provides
@PerDialog
fun provideEventProcessRemoteChannel(
proxy: EventProxy
): EventProcessDropFilesRemoteChannel = EventProcessDropFilesMiddlewareChannel(events = proxy)
@Provides
@PerDialog
fun provideEventProcessDateChannel(
channel: EventProcessDropFilesRemoteChannel
): EventProcessDropFilesChannel = EventProcessDropFilesDateChannel(channel = channel)
@Module
interface Declarations {
@PerDialog
@ -61,4 +80,5 @@ interface AddToAnytypeDependencies : ComponentDependencies {
fun fileSharer(): FileSharer
fun permissions(): UserPermissionProvider
fun analyticSpaceHelper(): AnalyticSpaceHelperDelegate
fun eventProxy(): EventProxy
}

View file

@ -1,5 +1,12 @@
package com.anytypeio.anytype.ui.sharing
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -12,10 +19,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.Composable
@ -28,6 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -37,28 +50,44 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import coil.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonPrimaryLoading
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.TitleInter15
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_utils.ui.MultipleEventCutter
import com.anytypeio.anytype.core_utils.ui.get
import com.anytypeio.anytype.presentation.sharing.AddToAnytypeViewModel
import com.anytypeio.anytype.presentation.sharing.AddToAnytypeViewModel.SpaceView
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
@Preview
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
@Composable
fun AddToAnytypeScreenUrlPreview() {
AddToAnytypeScreen(
data = SharingData.Url("https://en.wikipedia.org/wiki/Walter_Benjamin"),
onCancelClicked = {},
onDoneClicked = {},
spaces = emptyList(),
onAddClicked = {},
spaces = listOf(
SpaceView(
obj = ObjectWrapper.SpaceView(map = mapOf("name" to "Space 1")),
isSelected = true,
icon = SpaceIconView.Gradient(from = "#FF0000", to = "#00FF00")
)
),
onSelectSpaceClicked = {},
content = "https://en.wikipedia.org/wiki/Walter_Benjamin"
onOpenClicked = {},
content = "https://en.wikipedia.org/wiki/Walter_Benjamin",
progressState = AddToAnytypeViewModel.ProgressState.Done(""),
onCancelProcessClicked = {}
//progressState = AddToAnytypeViewModel.ProgressState.Error(" I understand that contributing to this repository will require me to agree with the CLA I understand that contributing to this repository will require me to agree with the CLA\n")
//progressState = AddToAnytypeViewModel.ProgressState.Progress(processId = "dasda", progress = 0.8f)
)
}
@ -68,10 +97,23 @@ fun AddToAnytypeScreenNotePreview() {
AddToAnytypeScreen(
data = SharingData.Text("The Work of Art in the Age of its Technological Reproducibility"),
onCancelClicked = {},
onDoneClicked = {},
spaces = emptyList(),
onAddClicked = {},
spaces = listOf(
SpaceView(
obj = ObjectWrapper.SpaceView(map = mapOf()),
isSelected = false,
icon = SpaceIconView.Gradient(from = "#FF0000", to = "#00FF00")
)
),
onSelectSpaceClicked = {},
content = ""
content = "",
progressState = AddToAnytypeViewModel.ProgressState.Progress(
processId = "dasda",
progress = 0.8f,
wrapperObjId = ""
),
onOpenClicked = {},
onCancelProcessClicked = {}
)
}
@ -80,9 +122,12 @@ fun AddToAnytypeScreen(
content: String,
spaces: List<SpaceView>,
data: SharingData,
progressState: AddToAnytypeViewModel.ProgressState,
onCancelClicked: () -> Unit,
onDoneClicked: (SaveAsOption) -> Unit,
onSelectSpaceClicked: (SpaceView) -> Unit
onCancelProcessClicked: (Id) -> Unit,
onAddClicked: (SaveAsOption) -> Unit,
onSelectSpaceClicked: (SpaceView) -> Unit,
onOpenClicked: (Id) -> Unit
) {
var isSaveAsMenuExpanded by remember { mutableStateOf(false) }
val items = when (data) {
@ -95,7 +140,7 @@ fun AddToAnytypeScreen(
}
var selectedIndex by remember {
mutableStateOf(
when(data) {
when (data) {
is SharingData.Url -> SAVE_AS_BOOKMARK
is SharingData.Image -> SAVE_AS_IMAGE
is SharingData.File -> SAVE_AS_FILE
@ -112,10 +157,9 @@ fun AddToAnytypeScreen(
}
Header()
DataSection(content)
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.height(76.dp)
.noRippleClickable {
throttler.processEvent {
isSaveAsMenuExpanded = !isSaveAsMenuExpanded
@ -140,8 +184,7 @@ fun AddToAnytypeScreen(
else -> stringResource(id = R.string.sharing_menu_save_as_note_option)
},
modifier = Modifier
.align(Alignment.BottomStart)
.padding(bottom = 14.dp, start = 20.dp),
.padding(top = 6.dp, start = 20.dp, bottom = 14.dp),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
@ -164,7 +207,7 @@ fun AddToAnytypeScreen(
isSaveAsMenuExpanded = false
}
) {
when(s) {
when (s) {
SAVE_AS_BOOKMARK -> {
Text(
text = stringResource(id = R.string.sharing_menu_save_as_bookmark_option),
@ -172,6 +215,7 @@ fun AddToAnytypeScreen(
color = colorResource(id = R.color.text_primary)
)
}
SAVE_AS_NOTE -> {
Text(
text = stringResource(id = R.string.sharing_menu_save_as_note_option),
@ -179,6 +223,7 @@ fun AddToAnytypeScreen(
color = colorResource(id = R.color.text_primary)
)
}
else -> {
// Draw nothing
}
@ -194,6 +239,7 @@ fun AddToAnytypeScreen(
}
}
}
com.anytypeio.anytype.core_ui.foundation.Divider(paddingEnd = 20.dp, paddingStart = 20.dp)
val selected = spaces.firstOrNull { it.isSelected }
if (selected != null) {
CurrentSpaceSection(
@ -209,11 +255,116 @@ fun AddToAnytypeScreen(
onSelectSpaceClicked = onSelectSpaceClicked
)
}
Spacer(modifier = Modifier.height(20.dp))
Buttons(onCancelClicked, onDoneClicked, selectedIndex)
DefaultLinearProgressIndicator(progressState = progressState)
when (progressState) {
is AddToAnytypeViewModel.ProgressState.Done -> {
ButtonsDone(
progressState = progressState,
onCancelClicked = onCancelClicked,
onOpenClicked = onOpenClicked
)
}
is AddToAnytypeViewModel.ProgressState.Error -> {
Buttons(
onCancelClicked = onCancelClicked,
selectedIndex = selectedIndex,
progressState = progressState,
onAddClicked = onAddClicked
)
}
AddToAnytypeViewModel.ProgressState.Init -> {
Buttons(
onCancelClicked = onCancelClicked,
selectedIndex = selectedIndex,
progressState = progressState,
onAddClicked = onAddClicked
)
}
is AddToAnytypeViewModel.ProgressState.Progress -> {
ButtonsProgress(
onCancelProcessClicked = onCancelProcessClicked,
progressState = progressState,
)
}
}
}
}
@Composable
private fun DefaultLinearProgressIndicator(progressState: AddToAnytypeViewModel.ProgressState) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(46.dp),
contentAlignment = Alignment.Center
) {
val visible = progressState is AddToAnytypeViewModel.ProgressState.Progress
AnimatedVisibility(
visible = visible,
modifier = Modifier,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut() + slideOutVertically { it }
) {
if (progressState is AddToAnytypeViewModel.ProgressState.Progress) {
Indicator(progress = progressState.progress)
}
}
val doneVisibility = progressState is AddToAnytypeViewModel.ProgressState.Done
AnimatedVisibility(
visible = doneVisibility,
modifier = Modifier,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut() + slideOutVertically { it }
) {
Text(
text = stringResource(id = R.string.sharing_menu_add_to_anytype_success),
style = Caption1Medium,
color = colorResource(id = R.color.palette_system_green),
modifier = Modifier.padding(top = 4.dp, start = 20.dp, end = 20.dp)
)
}
val errorVisible = progressState is AddToAnytypeViewModel.ProgressState.Error
AnimatedVisibility(
visible = errorVisible,
modifier = Modifier,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut() + slideOutVertically { it }
) {
if (progressState is AddToAnytypeViewModel.ProgressState.Error) {
Text(
text = stringResource(
id = R.string.sharing_menu_add_to_anytype_error,
progressState.error
),
style = Caption1Medium,
maxLines = 2,
color = colorResource(id = R.color.palette_dark_red),
modifier = Modifier.padding(horizontal = 20.dp)
)
}
}
}
}
@Composable
private fun Indicator(progress: Float) {
val animatedProgress = animateFloatAsState(
targetValue = progress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = ""
).value
LinearProgressIndicator(
progress = animatedProgress,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
.height(6.dp)
.fillMaxWidth()
.padding(horizontal = 20.dp),
backgroundColor = colorResource(id = R.color.shape_tertiary),
strokeCap = StrokeCap.Round
)
}
@Composable
private fun DataSection(content: String) {
Box(
@ -241,22 +392,24 @@ private fun DataSection(content: String) {
text = content,
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.padding(
top = 30.dp,
start = 16.dp,
end = 16.dp,
bottom = 10.dp
),
modifier = Modifier
.padding(
top = 30.dp,
start = 16.dp,
end = 16.dp,
bottom = 10.dp
)
.verticalScroll(rememberScrollState()),
maxLines = 5
)
}
}
@Composable
private fun Buttons(
private fun ButtonsDone(
progressState: AddToAnytypeViewModel.ProgressState.Done,
onCancelClicked: () -> Unit,
onDoneClicked: (SaveAsOption) -> Unit,
selectedIndex: Int
onOpenClicked: (Id) -> Unit
) {
Row(
modifier = Modifier
@ -273,11 +426,74 @@ private fun Buttons(
)
Spacer(modifier = Modifier.width(12.dp))
ButtonPrimary(
onClick = { onDoneClicked(selectedIndex) },
onClick = {
onOpenClicked(progressState.wrapperObjId)
},
size = ButtonSize.Large,
text = stringResource(id = R.string.done),
text = stringResource(id = R.string.sharing_menu_btn_open),
modifier = Modifier.weight(1.0f),
)
}
}
@Composable
private fun ButtonsProgress(
onCancelProcessClicked: (Id) -> Unit,
progressState: AddToAnytypeViewModel.ProgressState.Progress
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(68.dp)
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
ButtonSecondary(
onClick = { onCancelProcessClicked(progressState.processId) },
size = ButtonSize.Large,
text = stringResource(id = R.string.cancel),
modifier = Modifier.weight(1.0f)
)
Spacer(modifier = Modifier.width(12.dp))
ButtonPrimaryLoading(
size = ButtonSize.Large,
text = stringResource(id = R.string.sharing_menu_btn_add),
modifierBox = Modifier.weight(1.0f),
modifierButton = Modifier.fillMaxWidth(),
loading = true
)
}
}
@Composable
private fun Buttons(
onCancelClicked: () -> Unit,
onAddClicked: (SaveAsOption) -> Unit,
selectedIndex: Int,
progressState: AddToAnytypeViewModel.ProgressState
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(68.dp)
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
ButtonSecondary(
onClick = onCancelClicked,
size = ButtonSize.Large,
text = stringResource(id = R.string.cancel),
modifier = Modifier.weight(1.0f)
)
Spacer(modifier = Modifier.width(12.dp))
ButtonPrimaryLoading(
onClick = { onAddClicked(selectedIndex) },
size = ButtonSize.Large,
text = stringResource(id = R.string.sharing_menu_btn_add),
modifierBox = Modifier.weight(1.0f),
modifierButton = Modifier.fillMaxWidth(),
loading = progressState is AddToAnytypeViewModel.ProgressState.Progress
)
}
}
@ -292,10 +508,10 @@ private fun CurrentSpaceSection(
val throttler = remember {
MultipleEventCutter.Companion.get(interval = DROPDOWN_MENU_VISIBILITY_WINDOW_INTERVAL)
}
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.height(76.dp)
.wrapContentHeight()
.noRippleClickable {
throttler.processEvent {
isSpaceSelectMenuExpanded = true
@ -309,29 +525,26 @@ private fun CurrentSpaceSection(
style = Caption1Medium,
color = colorResource(id = R.color.text_secondary)
)
val hasIcon = icon is SpaceIconView.Gradient || icon is SpaceIconView.Image
if (icon != null && hasIcon) {
SmallSpaceIcon(
icon = icon,
modifier = Modifier
.padding(
start = 20.dp,
bottom = 17.dp
)
.align(Alignment.BottomStart)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
val hasIcon = icon is SpaceIconView.Gradient || icon is SpaceIconView.Image
if (icon != null && hasIcon) {
SmallSpaceIcon(
icon = icon,
modifier = Modifier.padding(end = 8.dp)
)
}
Text(
text = name,
modifier = Modifier,
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
Text(
text = name,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(
bottom = 14.dp,
start = if (hasIcon) 44.dp else 20.dp
),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
DropdownMenu(
expanded = isSpaceSelectMenuExpanded,
onDismissRequest = {
@ -365,6 +578,7 @@ private fun CurrentSpaceSection(
}
}
}
com.anytypeio.anytype.core_ui.foundation.Divider(paddingEnd = 20.dp, paddingStart = 20.dp)
}
@Composable
@ -378,7 +592,7 @@ private fun Header() {
text = stringResource(R.string.sharing_menu_add_to_anytype_header_title),
color = colorResource(id = R.color.text_primary),
modifier = Modifier.align(Alignment.Center),
style = TitleInter15
style = Title2
)
}
}
@ -389,7 +603,7 @@ private fun SmallSpaceIcon(
icon: SpaceIconView,
modifier: Modifier
) {
val size = 18.dp
val size = 20.dp
when (icon) {
is SpaceIconView.Image -> {
Image(
@ -419,6 +633,7 @@ private fun SmallSpaceIcon(
.background(gradient)
)
}
else -> {
// Draw nothing.
}
@ -435,30 +650,33 @@ typealias SaveAsOption = Int
sealed class SharingData {
abstract val data: String
data class Url(val url: String) : SharingData() {
override val data: String
get() = url
}
data class Text(val raw: String) : SharingData() {
override val data: String
get() = raw
}
data class Image(val uri: String) : SharingData() {
override val data: String
get() = uri
}
data class Images(val uris: List<String>): SharingData() {
data class Images(val uris: List<String>) : SharingData() {
override val data: String
get() = uris.toString()
}
data class Files(val uris: List<String>): SharingData() {
data class Files(val uris: List<String>) : SharingData() {
override val data: String
get() = uris.toString()
}
data class File(val uri: String): SharingData() {
data class File(val uri: String) : SharingData() {
override val data: String
get() = uri
}

View file

@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.argStringList
import com.anytypeio.anytype.core_utils.ext.getFormattedDateTime
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
@ -24,6 +25,7 @@ import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.sharing.AddToAnytypeViewModel
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.settings.typography
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.flow.map
@ -79,24 +81,29 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
}
}.collectAsState(initial = "").value,
data = sharedData,
onDoneClicked = { option ->
onAddClicked = { option ->
when(option) {
SAVE_AS_BOOKMARK -> vm.onCreateBookmark(url = sharedData.data)
SAVE_AS_NOTE -> vm.onCreateNote(sharedData.data)
SAVE_AS_IMAGE -> vm.onShareMedia(listOf(sharedData.data))
SAVE_AS_FILE -> vm.onShareMedia(listOf(sharedData.data))
SAVE_AS_IMAGES -> {
SAVE_AS_IMAGES, SAVE_AS_IMAGE -> {
val formattedDateTime = getFormattedDateTime(Locale.getDefault())
val objTitle =
getString(R.string.sharing_media_wrapper_object_title, formattedDateTime)
val data = sharedData
if (data is SharingData.Images) {
vm.onShareMedia(uris = data.uris)
vm.onShareMedia(uris = data.uris, wrapperObjTitle = objTitle)
} else {
toast("Unexpected data format")
}
}
SAVE_AS_FILES -> {
val formattedDateTime = getFormattedDateTime(Locale.getDefault())
val objTitle =
getString(R.string.sharing_files_wrapper_object_title, formattedDateTime)
val data = sharedData
if (data is SharingData.Files) {
vm.onShareMedia(uris = data.uris)
vm.onShareMedia(uris = data.uris, wrapperObjTitle = objTitle)
} else {
toast("Unexpected data format")
}
@ -109,7 +116,10 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
}
},
spaces = vm.spaceViews.collectAsStateWithLifecycle().value,
onSelectSpaceClicked = { vm.onSelectSpaceClicked(it) }
onSelectSpaceClicked = { vm.onSelectSpaceClicked(it) },
progressState = vm.progressState.collectAsStateWithLifecycle().value,
onOpenClicked = vm::proceedWithNavigation,
onCancelProcessClicked = { processId -> }
)
LaunchedEffect(Unit) {
vm.navigation.collect { nav ->

View file

@ -30,6 +30,14 @@ sealed class Command {
val type: Block.Content.File.Type?
)
class FileDrop(
val space: SpaceId,
val ctx: Id,
val dropTarget: Id,
val blockPosition: Position,
val localFilePaths: List<String>
)
class DownloadFile(
val path: String,
val objectId: Id

View file

@ -50,6 +50,7 @@ import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import timber.log.Timber
@ -477,4 +478,31 @@ fun BaseBottomSheetComposeFragment.setupBottomSheetBehavior(paddingTop: Int) {
state = BottomSheetBehavior.STATE_EXPANDED
skipCollapsed = true
}
}
fun getLocalizedDateTimePattern(locale: Locale): String {
// Get DateFormat instances for both date and time
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale)
val timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale)
// Convert to SimpleDateFormat to extract the pattern
val datePattern = (dateFormat as? SimpleDateFormat)?.toPattern()
val timePattern = (timeFormat as? SimpleDateFormat)?.toPattern()
// Combine date and time patterns
return "$datePattern $timePattern"
}
fun getFormattedDateTime(locale: Locale): String {
// Get the current date and time
val currentDate = Date()
// Get the localized pattern
val localizedPattern = getLocalizedDateTimePattern(locale)
// Create a formatter with the localized pattern
val simpleDateFormat = SimpleDateFormat(localizedPattern, locale)
// Format the current date and time
return simpleDateFormat.format(currentDate)
}

View file

@ -265,6 +265,10 @@ class BlockDataRepository(
command: Command.UploadFile
): ObjectWrapper.File = remote.uploadFile(command)
override suspend fun fileDrop(command: Command.FileDrop): Payload {
return remote.fileDrop(command)
}
override suspend fun downloadFile(
command: Command.DownloadFile
): String = remote.downloadFile(command)

View file

@ -89,6 +89,7 @@ interface BlockRemote {
suspend fun copy(command: Command.Copy): Response.Clipboard.Copy
suspend fun uploadFile(command: Command.UploadFile): ObjectWrapper.File
suspend fun fileDrop(command: Command.FileDrop): Payload
suspend fun downloadFile(command: Command.DownloadFile): String
suspend fun setRelationKey(command: Command.SetRelationKey): Payload

View file

@ -41,6 +41,7 @@ import com.anytypeio.anytype.domain.page.Undo
interface BlockRepository {
suspend fun uploadFile(command: Command.UploadFile): ObjectWrapper.File
suspend fun fileDrop(command: Command.FileDrop): Payload
suspend fun downloadFile(command: Command.DownloadFile): String
suspend fun move(command: Command.Move): Payload

View file

@ -0,0 +1,36 @@
package com.anytypeio.anytype.domain.media
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NO_VALUE
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import javax.inject.Inject
class FileDrop @Inject constructor(
private val repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<FileDrop.Params, Payload>(dispatchers.io) {
override suspend fun doWork(params: Params): Payload = repo.fileDrop(
command = Command.FileDrop(
ctx = params.ctx,
space = params.space,
dropTarget = params.dropTarget,
blockPosition = params.blockPosition,
localFilePaths = params.localFilePaths
)
)
data class Params(
val space: SpaceId,
val ctx: Id,
val dropTarget: Id = NO_VALUE,
val blockPosition: Position = Position.NONE,
val localFilePaths: List<String>
)
}

View file

@ -1212,7 +1212,13 @@
<string name="sharing_menu_save_as_files_option">Files</string>
<string name="sharing_menu_data">Data</string>
<string name="sharing_menu_add_to_anytype_header_title">Add to Anytype</string>
<string name="sharing_menu_add_to_anytype_error">Anytype file upload error: %1$s</string>
<string name="sharing_menu_add_to_anytype_success">All files have been successfully uploaded</string>
<string name="sharing_menu_toast_object_added">New object is added to the space \'%1$s\'</string>
<string name="sharing_media_wrapper_object_title">Shared Media, %1$s, this object is auxiliary and can be deleted; all media or files will remain in Space.</string>
<string name="sharing_files_wrapper_object_title">Shared Files, %1$s, this object is auxiliary and can be deleted; all media or files will remain in Space.</string>
<string name="sharing_menu_btn_add">Add</string>
<string name="sharing_menu_btn_open">Open</string>
<!--endregion-->

View file

@ -230,6 +230,10 @@ class BlockMiddleware(
command: Command.UploadFile
): ObjectWrapper.File = middleware.fileUpload(command)
override suspend fun fileDrop(command: Command.FileDrop): Payload {
return middleware.fileDrop(command)
}
override suspend fun downloadFile(
command: Command.DownloadFile
): String = middleware.fileDownload(command).localPath

View file

@ -813,7 +813,7 @@ class Middleware @Inject constructor(
val request = Rpc.File.Upload.Request(
localPath = command.path,
type = type,
spaceId = command.space?.id.orEmpty()
spaceId = command.space.id
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.fileUpload(request)
@ -821,6 +821,20 @@ class Middleware @Inject constructor(
return ObjectWrapper.File(response.details.orEmpty())
}
@Throws(Exception::class)
fun fileDrop(command: Command.FileDrop): Payload {
val request = Rpc.File.Drop.Request(
contextId = command.ctx,
dropTargetId = command.dropTarget,
position = command.blockPosition.toMiddlewareModel(),
localFilePaths = command.localFilePaths
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.fileDrop(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.event.toPayload()
}
@Throws(Exception::class)
fun fileDownload(command: Command.DownloadFile): Rpc.File.Download.Response {
val request = Rpc.File.Download.Request(

View file

@ -200,6 +200,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun spaceUsage(request: Rpc.File.SpaceUsage.Request): Rpc.File.SpaceUsage.Response
@Throws(Exception::class)
fun fileDrop(request: Rpc.File.Drop.Request): Rpc.File.Drop.Response
//endregion
//region UNSPLASH commands

View file

@ -633,6 +633,17 @@ class MiddlewareServiceImplementation @Inject constructor(
}
}
override fun fileDrop(request: Rpc.File.Drop.Request): Rpc.File.Drop.Response {
val encoded = Service.fileDrop(Rpc.File.Drop.Request.ADAPTER.encode(request))
val response = Rpc.File.Drop.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.File.Drop.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun fileDownload(request: Rpc.File.Download.Request): Rpc.File.Download.Response {
val encoded = Service.fileDownload(Rpc.File.Download.Request.ADAPTER.encode(request))
val response = Rpc.File.Download.Response.ADAPTER.decode(encoded)

View file

@ -11,7 +11,6 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.CLICK_ONBOARDING_TO
import com.anytypeio.anytype.analytics.base.EventsPropertiesKey
import com.anytypeio.anytype.analytics.event.EventAnalytics
import com.anytypeio.anytype.analytics.props.Props
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
import com.anytypeio.anytype.core_models.NO_VALUE
@ -20,16 +19,20 @@ import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.Process.Event
import com.anytypeio.anytype.core_models.Process.State
import com.anytypeio.anytype.core_utils.ext.msg
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.FileSharer
import com.anytypeio.anytype.domain.media.FileDrop
import com.anytypeio.anytype.domain.media.UploadFile
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.Permissions
import com.anytypeio.anytype.domain.objects.CreateBookmarkObject
import com.anytypeio.anytype.domain.objects.CreatePrefilledNote
import com.anytypeio.anytype.domain.spaces.GetSpaceViews
import com.anytypeio.anytype.domain.workspace.EventProcessDropFilesChannel
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.common.BaseViewModel
@ -40,6 +43,8 @@ import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.spaces.spaceIcon
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -58,10 +63,11 @@ class AddToAnytypeViewModel(
private val urlBuilder: UrlBuilder,
private val awaitAccountStartManager: AwaitAccountStartManager,
private val analytics: Analytics,
private val uploadFile: UploadFile,
private val fileSharer: FileSharer,
private val permissions: Permissions,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val fileDrop: FileDrop,
private val eventProcessChannel: EventProcessDropFilesChannel
) : BaseViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
private val selectedSpaceId = MutableStateFlow(NO_VALUE)
@ -71,9 +77,12 @@ class AddToAnytypeViewModel(
val navigation = MutableSharedFlow<OpenObjectNavigation>()
val spaceViews = MutableStateFlow<List<SpaceView>>(emptyList())
val commands = MutableSharedFlow<Command>()
val progressState = MutableStateFlow<ProgressState>(ProgressState.Init)
val state = MutableStateFlow<ViewState>(ViewState.Init)
private var progressJob: Job? = null
init {
viewModelScope.launch {
analytics.registerEvent(
@ -136,7 +145,70 @@ class AddToAnytypeViewModel(
}
}
fun onShareMedia(uris: List<String>) {
private fun subscribeToEventProcessChannel(wrapperObjId: Id) {
if (progressJob?.isActive == true) {
progressJob?.cancel()
}
progressJob = viewModelScope.launch {
eventProcessChannel.observe()
.collect { events ->
events.forEach { event ->
when (event) {
is Event.DropFiles.New -> {
val currentProgressState = progressState.value
if (currentProgressState is ProgressState.Init
&& event.process.state == State.RUNNING
) {
progressState.value = ProgressState.Progress(
processId = event.process.id,
progress = 0f,
wrapperObjId = wrapperObjId
)
} else {
//some process is already running
}
}
is Event.DropFiles.Update -> {
val currentProgressState = progressState.value
val newProcess = event.process
if (currentProgressState is ProgressState.Progress
&& currentProgressState.processId == event.process.id
&& newProcess.state == State.RUNNING
) {
val progress = newProcess.progress
val total = progress?.total
val done = progress?.done
progressState.value =
if (total != null && total != 0L && done != null) {
currentProgressState.copy(progress = done.toFloat() / total)
} else {
currentProgressState.copy(progress = 0f)
}
}
}
is Event.DropFiles.Done -> {
val currentProgressState = progressState.value
val newProcess = event.process
if (currentProgressState is ProgressState.Progress
&& event.process.state == State.DONE
&& newProcess.id == currentProgressState.processId
) {
progressState.value = currentProgressState.copy(progress = 1f)
delay(300)
progressState.value = ProgressState.Done(
wrapperObjId = currentProgressState.wrapperObjId
)
}
}
}
}
}
}
}
fun onShareMedia(uris: List<String>, wrapperObjTitle: String? = null) {
viewModelScope.launch(Dispatchers.IO) {
val targetSpaceView = spaceViews.value.firstOrNull { view ->
view.isSelected
@ -147,89 +219,98 @@ class AddToAnytypeViewModel(
val paths = uris.mapNotNull { uri ->
fileSharer.getPath(uri)
}
val files = mutableListOf<Id>()
paths.forEach { path ->
uploadFile.async(
UploadFile.Params(
path = path,
space = SpaceId(targetSpaceId),
// Temporary workaround to fix issue on the MW side.
type = Block.Content.File.Type.NONE
)
).fold(
onSuccess = { obj ->
files.add(obj.id)
},
onFailure = { e ->
Timber.e(e, "Error while uploading file").also {
sendToast(e.msg())
}
}
when (paths.size) {
0 -> sendToast("Could not get file paths")
else -> proceedWithCreatingWrapperObject(
filePaths = paths,
targetSpaceId = targetSpaceId,
wrapperObjTitle = wrapperObjTitle,
)
}
when (files.size) {
0 -> {
sendToast("Could not upload files")
}
}
}
}
1 -> {
// No need to create a wrapper object, opening file object directly instead
if (targetSpaceId == spaceManager.get()) {
navigation.emit(
OpenObjectNavigation.OpenEditor(
target = files.first(),
space = targetSpaceId
)
)
} else {
with(commands) {
emit(Command.ObjectAddToSpaceToast(targetSpaceView.obj.name))
emit(Command.Dismiss)
}
}
}
private suspend fun proceedWithCreatingWrapperObject(
filePaths: List<String>,
targetSpaceId: String,
wrapperObjTitle: String? = null
) {
val startTime = System.currentTimeMillis()
createPrefilledNote.async(
CreatePrefilledNote.Params(
text = wrapperObjTitle ?: EMPTY_STRING_VALUE,
space = targetSpaceId,
details = mapOf(
Relations.ORIGIN to ObjectOrigin.SHARING_EXTENSION.code.toDouble(),
),
)
).fold(
onSuccess = { wrapperObjId ->
viewModelScope.sendAnalyticsObjectCreateEvent(
analytics = analytics,
objType = MarketplaceObjectTypeIds.PAGE,
route = EventsDictionary.Routes.sharingExtension,
startTime = startTime,
spaceParams = provideParams(spaceManager.get())
)
proceedWithFilesDrop(
wrapperObjId = wrapperObjId,
filePaths = filePaths,
targetSpaceId = targetSpaceId
)
},
onFailure = {
Timber.d(it, "Error while creating page")
sendToast("Error while creating page: ${it.msg()}")
}
)
}
else -> {
// Creating a wrapper object for file objects
val startTime = System.currentTimeMillis()
createPrefilledNote.async(
CreatePrefilledNote.Params(
text = EMPTY_STRING_VALUE,
space = targetSpaceId,
details = mapOf(
Relations.ORIGIN to ObjectOrigin.SHARING_EXTENSION.code.toDouble()
),
attachments = files
)
).fold(
onSuccess = { result ->
sendAnalyticsObjectCreateEvent(
analytics = analytics,
objType = MarketplaceObjectTypeIds.NOTE,
route = EventsDictionary.Routes.sharingExtension,
startTime = startTime,
spaceParams = provideParams(spaceManager.get())
)
if (targetSpaceId == spaceManager.get()) {
navigation.emit(
OpenObjectNavigation.OpenEditor(
target = result,
space = targetSpaceId
)
)
} else {
with(commands) {
emit(Command.ObjectAddToSpaceToast(targetSpaceView.obj.name))
emit(Command.Dismiss)
}
}
},
onFailure = {
Timber.d(it, "Error while creating note")
sendToast("Error while creating note: ${it.msg()}")
}
)
}
private suspend fun proceedWithFilesDrop(
wrapperObjId: Id,
filePaths: List<String>,
targetSpaceId: String,
) {
subscribeToEventProcessChannel(wrapperObjId = wrapperObjId)
val params = FileDrop.Params(
ctx = wrapperObjId,
space = SpaceId(targetSpaceId),
localFilePaths = filePaths
)
fileDrop.async(params).fold(
onSuccess = { _ -> Timber.d("Files dropped successfully") },
onFailure = { e ->
Timber.e(e, "Error while dropping files").also {
sendToast(e.msg())
}
}
)
}
fun proceedWithNavigation(wrapperObjId: Id) {
val targetSpaceView = spaceViews.value.firstOrNull { view ->
view.isSelected
}
val targetSpaceId = targetSpaceView?.obj?.targetSpaceId
viewModelScope.launch {
Timber.d("proceedWithNavigation: $wrapperObjId, $targetSpaceId")
if (targetSpaceId == spaceManager.get()) {
Timber.d("proceedWithNavigation: OpenEditor")
delay(300)
navigation.emit(
OpenObjectNavigation.OpenEditor(
target = wrapperObjId,
space = targetSpaceId
)
)
} else {
Timber.d("proceedWithNavigation: ObjectAddToSpaceToast")
delay(300)
with(commands) {
emit(Command.ObjectAddToSpaceToast(targetSpaceView?.obj?.name))
emit(Command.Dismiss)
}
}
}
@ -381,10 +462,11 @@ class AddToAnytypeViewModel(
private val urlBuilder: UrlBuilder,
private val awaitAccountStartManager: AwaitAccountStartManager,
private val analytics: Analytics,
private val uploadFile: UploadFile,
private val fileSharer: FileSharer,
private val permissions: Permissions,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val fileDrop: FileDrop,
private val eventProcessChannel: EventProcessDropFilesChannel
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -396,10 +478,11 @@ class AddToAnytypeViewModel(
urlBuilder = urlBuilder,
awaitAccountStartManager = awaitAccountStartManager,
analytics = analytics,
uploadFile = uploadFile,
fileSharer = fileSharer,
permissions = permissions,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
fileDrop = fileDrop,
eventProcessChannel = eventProcessChannel
) as T
}
}
@ -411,17 +494,26 @@ class AddToAnytypeViewModel(
)
sealed class Command {
object Dismiss : Command()
data object Dismiss : Command()
data class ObjectAddToSpaceToast(
val spaceName: String?
) : Command()
}
sealed class ViewState {
object Init : ViewState()
data object Init : ViewState()
data class Default(val content: String) : ViewState()
}
sealed class ProgressState {
data object Init : ProgressState()
data class Progress(val wrapperObjId: Id, val processId: Id, val progress: Float) :
ProgressState()
data class Done(val wrapperObjId: Id) : ProgressState()
data class Error(val error: String) : ProgressState()
}
companion object {
const val FILE_NAME_SEPARATOR = ", "
}