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

DROID-1776 Gallery experience | Installation process + notification handling (#1022)

This commit is contained in:
Konstantin Ivanov 2024-03-21 15:08:20 +01:00 committed by GitHub
parent 7349cfe901
commit fd94eba92c
Signed by: github
GPG key ID: B5690EEEBB952194
12 changed files with 108 additions and 40 deletions

View file

@ -7,6 +7,8 @@ import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.workspace.EventProcessChannel
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModelFactory
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
@ -65,4 +67,6 @@ interface GalleryInstallationComponentDependencies : ComponentDependencies {
fun appCoroutineDispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
fun urlBuilder(): UrlBuilder
fun userPermissionProvider(): UserPermissionProvider
fun eventProcessChannel(): EventProcessChannel
}

View file

@ -30,6 +30,7 @@ import com.google.accompanist.navigation.material.ModalBottomSheetLayout
import com.google.accompanist.navigation.material.bottomSheet
import com.google.accompanist.navigation.material.rememberBottomSheetNavigator
import javax.inject.Inject
import timber.log.Timber
class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
@ -62,6 +63,7 @@ class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
override fun onStart() {
super.onStart()
jobs += subscribe(vm.command) { command ->
Timber.d("GalleryInstallationFragment command: $command")
when (command) {
GalleryInstallationNavigation.Main -> navController.navigate(
GalleryInstallationNavigation.Main.route
@ -70,6 +72,7 @@ class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
GalleryInstallationNavigation.Spaces.route
)
GalleryInstallationNavigation.Dismiss -> navController.popBackStack()
GalleryInstallationNavigation.Exit -> dismiss()
else -> {}
}
}

View file

@ -970,7 +970,7 @@ class BlockDataRepository(
return remote.downloadGalleryManifest(command)
}
override suspend fun importExperience(command: Command.ImportExperience): Payload {
return remote.importExperience(command)
override suspend fun importExperience(command: Command.ImportExperience) {
remote.importExperience(command)
}
}

View file

@ -415,5 +415,5 @@ interface BlockRemote {
suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink
suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo?
suspend fun importExperience(command: Command.ImportExperience): Payload
suspend fun importExperience(command: Command.ImportExperience)
}

View file

@ -458,5 +458,5 @@ interface BlockRepository {
suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink
suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo?
suspend fun importExperience(command: Command.ImportExperience): Payload
suspend fun importExperience(command: Command.ImportExperience)
}

View file

@ -1,7 +1,6 @@
package com.anytypeio.anytype.domain.gallery_experience
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
@ -11,16 +10,16 @@ import javax.inject.Inject
class ImportExperience @Inject constructor(
dispatchers: AppCoroutineDispatchers,
private val repo: BlockRepository
) : ResultInteractor<ImportExperience.Params, Payload>(dispatchers.io) {
) : ResultInteractor<ImportExperience.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params): Payload {
override suspend fun doWork(params: Params) {
val command = Command.ImportExperience(
space = params.spaceId,
url = params.url,
title = params.title,
isNewSpace = params.isNewSpace
)
return repo.importExperience(command)
repo.importExperience(command)
}
data class Params(

View file

@ -7,7 +7,7 @@ import com.anytypeio.anytype.presentation.spaces.SpaceIconView
sealed class GalleryInstallationState {
object Hidden : GalleryInstallationState()
object Loading : GalleryInstallationState()
data class Success(val info: ManifestInfo) : GalleryInstallationState()
data class Success(val info: ManifestInfo, val isLoading: Boolean = false) : GalleryInstallationState()
}
data class GalleryInstallationSpacesState(
@ -21,6 +21,7 @@ sealed class GalleryInstallationNavigation(val route: String) {
object Success : GalleryInstallationNavigation("success")
object Error : GalleryInstallationNavigation("error")
object Dismiss : GalleryInstallationNavigation("")
object Exit : GalleryInstallationNavigation("exit")
}
data class GallerySpaceView(

View file

@ -1,6 +1,5 @@
package com.anytypeio.anytype.gallery_experience.screens
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
@ -30,8 +29,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
@ -49,7 +46,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonPrimaryLoading
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption1Regular
@ -62,13 +59,19 @@ fun GalleryInstallationScreen(
state: GalleryInstallationState,
onInstallClicked: () -> Unit
) {
val brush = Brush.verticalGradient(
listOf(
colorResource(id = R.color.background_highlighted),
Color.Transparent
)
)
Box(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
.fillMaxWidth()
.wrapContentHeight()
.background(
color = colorResource(id = R.color.background_secondary),
brush = brush,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
),
) {
@ -83,6 +86,7 @@ fun GalleryInstallationScreen(
GalleryInstallationState.Loading -> {
LoadingScreen()
}
is GalleryInstallationState.Success -> {
SuccessScreen(state, onInstallClicked)
}
@ -94,25 +98,26 @@ fun GalleryInstallationScreen(
@Composable
private fun LoadingScreen() {
val infiniteTransition = rememberInfiniteTransition(label = "")
val translateAnim by infiniteTransition.animateFloat(
val colorStart = colorResource(id = R.color.background_secondary)
val colorEnd = colorResource(id = R.color.shape_secondary)
val shimmerColorShades = listOf(
colorStart,
colorEnd,
colorStart
)
val shimmerAnimation = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 200,
easing = LinearEasing
),
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Restart
), label = ""
)
val brush = Brush.linearGradient(
colors = listOf(
Color(0xFFF5F5F5),
Color(0xFFEBEBEB),
Color(0xFFF5F5F5)
),
start = Offset(10f, 10f),
end = Offset(translateAnim, translateAnim)
colors = shimmerColorShades,
start = Offset.Zero,
end = Offset(x = shimmerAnimation.value, y = 0F)
)
Spacer(modifier = Modifier.height(24.dp))
Box(
@ -172,7 +177,11 @@ private fun SuccessScreen(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.padding(horizontal = 20.dp),
.padding(horizontal = 20.dp)
.background(
color = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(8.dp)
),
state = pagerState
) { index ->
val screenshotUrl = state.info.screenshots[index]
@ -247,8 +256,9 @@ private fun SuccessScreen(
}
}
Spacer(modifier = Modifier.height(39.dp))
ButtonPrimary(
modifier = Modifier
ButtonPrimaryLoading(
loading = state.isLoading,
modifierButton = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
onClick = { onInstallClicked() },
@ -292,7 +302,14 @@ private fun GalleryInstallationScreenPreview() {
screenshots = listOf("1", "2", "3", "4"),
downloadLink = "lobortis",
fileSize = 1213,
categories = listOf("tag1", "tag2", "tag3312212112", "tag421312312", "tag5", "tag6"),
categories = listOf(
"tag1",
"tag2",
"tag3312212112",
"tag421312312",
"tag5",
"tag6"
),
language = "nisi"
)
)

View file

@ -5,14 +5,17 @@ import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Process
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.gallery_experience.DownloadGalleryManifest
import com.anytypeio.anytype.domain.gallery_experience.ImportExperience
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.spaces.CreateSpace
import com.anytypeio.anytype.domain.spaces.GetSpaceViews
import com.anytypeio.anytype.domain.workspace.EventProcessChannel
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationNavigation
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationSpacesState
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationState
@ -31,7 +34,9 @@ class GalleryInstallationViewModel(
private val getSpaceViews: GetSpaceViews,
private val createSpace: CreateSpace,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider
private val spaceGradientProvider: SpaceGradientProvider,
private val userPermissionProvider: UserPermissionProvider,
private val eventProcessChannel: EventProcessChannel
) : ViewModel() {
val mainState = MutableStateFlow<GalleryInstallationState>(GalleryInstallationState.Loading)
@ -39,6 +44,8 @@ class GalleryInstallationViewModel(
MutableStateFlow(GalleryInstallationSpacesState(emptyList(), false))
val command = MutableStateFlow<GalleryInstallationNavigation?>(null)
private val MAX_SPACES = 10
init {
Timber.d("GalleryInstallationViewModel init, viewModelParams: $viewModelParams")
downloadGalleryManifest()
@ -70,11 +77,12 @@ class GalleryInstallationViewModel(
getSpaceViews.async(Unit).fold(
onSuccess = { spaces ->
Timber.d("GetSpaceViews success, spaceViews: $spaces")
val filteredSpaces = filterSpacesByPermissions(spaces)
spacesViewState.value = GalleryInstallationSpacesState(
spaces = spaces.map {
spaces = filteredSpaces.map {
it.toView(urlBuilder, spaceGradientProvider)
},
isNewButtonVisible = true
isNewButtonVisible = filteredSpaces.size < MAX_SPACES
)
command.value = GalleryInstallationNavigation.Spaces
},
@ -86,8 +94,11 @@ class GalleryInstallationViewModel(
}
fun onNewSpaceClick() {
subscribeToEventProcessChannel()
command.value = GalleryInstallationNavigation.Dismiss
val manifestInfo = (mainState.value as? GalleryInstallationState.Success)?.info ?: return
mainState.value =
(mainState.value as? GalleryInstallationState.Success)?.copy(isLoading = true) ?: return
val params = CreateSpace.Params(
details = mapOf(
Relations.NAME to manifestInfo.name,
@ -112,10 +123,19 @@ class GalleryInstallationViewModel(
}
fun onSpaceClick(space: GallerySpaceView) {
subscribeToEventProcessChannel()
Timber.d("onSpaceClick, space: $space")
command.value = GalleryInstallationNavigation.Dismiss
mainState.value =
(mainState.value as? GalleryInstallationState.Success)?.copy(isLoading = true) ?: return
val manifestInfo = (mainState.value as? GalleryInstallationState.Success)?.info ?: return
val spaceId = space.obj.targetSpaceId
if (spaceId == null) {
Timber.e("onSpaceClick, spaceId is null")
return
}
proceedWithInstallation(
spaceId = SpaceId(space.obj.id),
spaceId = SpaceId(spaceId),
isNewSpace = false,
manifestInfo = manifestInfo
)
@ -150,6 +170,25 @@ class GalleryInstallationViewModel(
}
}
private fun subscribeToEventProcessChannel() {
viewModelScope.launch {
eventProcessChannel.observe().collect { events ->
Timber.d("EventProcessChannel events: $events")
if (events.any { it is Process.Event.Done && it.process?.type == Process.Type.IMPORT }) {
command.value = GalleryInstallationNavigation.Exit
}
}
}
}
private fun filterSpacesByPermissions(spaces: List<ObjectWrapper.SpaceView>): List<ObjectWrapper.SpaceView> {
return spaces.filter {
val targetSpaceId = it.targetSpaceId ?: return@filter false
val userPermissions = userPermissionProvider.get(SpaceId(targetSpaceId))
userPermissions?.isOwnerOrEditor() == true
}
}
data class ViewModelParams(
val deepLinkType: String,
val deepLinkSource: String

View file

@ -6,8 +6,10 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.gallery_experience.DownloadGalleryManifest
import com.anytypeio.anytype.domain.gallery_experience.ImportExperience
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.spaces.CreateSpace
import com.anytypeio.anytype.domain.spaces.GetSpaceViews
import com.anytypeio.anytype.domain.workspace.EventProcessChannel
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
@ -19,7 +21,9 @@ class GalleryInstallationViewModelFactory @Inject constructor(
private val getSpaceViews: GetSpaceViews,
private val createSpace: CreateSpace,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider
private val spaceGradientProvider: SpaceGradientProvider,
private val userPermissionProvider: UserPermissionProvider,
private val eventProcessChannel: EventProcessChannel
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -31,7 +35,9 @@ class GalleryInstallationViewModelFactory @Inject constructor(
getSpaceViews = getSpaceViews,
urlBuilder = urlBuilder,
spaceGradientProvider = spaceGradientProvider,
createSpace = createSpace
createSpace = createSpace,
userPermissionProvider = userPermissionProvider,
eventProcessChannel = eventProcessChannel
) as T
}
}

View file

@ -932,7 +932,7 @@ class BlockMiddleware(
return middleware.downloadGalleryManifest(command)
}
override suspend fun importExperience(command: Command.ImportExperience): Payload {
return middleware.importExperience(command)
override suspend fun importExperience(command: Command.ImportExperience) {
middleware.importExperience(command)
}
}

View file

@ -2517,7 +2517,7 @@ class Middleware @Inject constructor(
@Throws(Exception::class)
fun importExperience(
command: Command.ImportExperience
): Payload {
) {
val request = Rpc.Object.ImportExperience.Request(
spaceId = command.space.id,
url = command.url,
@ -2527,7 +2527,6 @@ class Middleware @Inject constructor(
if (BuildConfig.DEBUG) logRequest(request)
val response = service.objectImportExperience(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.event.toPayload()
}
private fun logRequest(any: Any) {