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 | Enhancement | Resolving deep link (#1011)

This commit is contained in:
Konstantin Ivanov 2024-03-19 13:36:00 +01:00 committed by GitHub
parent 7454fc8961
commit 9104566899
Signed by: github
GPG key ID: B5690EEEBB952194
28 changed files with 550 additions and 40 deletions

View file

@ -107,6 +107,7 @@ import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectModule
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetSourceModule
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetTypeModule
import com.anytypeio.anytype.di.main.MainComponent
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.presentation.multiplayer.RequestJoinSpaceViewModel
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel
import com.anytypeio.anytype.presentation.multiplayer.SpaceJoinRequestViewModel
@ -1076,9 +1077,13 @@ class ComponentManager(
DaggerPaymentsComponent.factory().create(findComponentDependencies())
}
val galleryInstallationsComponent = Component {
DaggerGalleryInstallationComponent.factory().create(findComponentDependencies())
}
val galleryInstallationsComponent =
ComponentWithParams { params: GalleryInstallationViewModel.ViewModelParams ->
DaggerGalleryInstallationComponent.builder()
.withDependencies(findComponentDependencies())
.withParams(params)
.build()
}
class Component<T>(private val builder: () -> T) {

View file

@ -4,11 +4,18 @@ import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
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.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModelFactory
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
@Component(
dependencies = [GalleryInstallationComponentDependencies::class],
@ -20,9 +27,13 @@ import dagger.Module
@PerScreen
interface GalleryInstallationComponent {
@Component.Factory
interface Factory {
fun create(dependencies: GalleryInstallationComponentDependencies): GalleryInstallationComponent
@Component.Builder
interface Builder {
fun withDependencies(dependencies: GalleryInstallationComponentDependencies): Builder
@BindsInstance
fun withParams(params: GalleryInstallationViewModel.ViewModelParams): Builder
fun build(): GalleryInstallationComponent
}
fun inject(fragment: GalleryInstallationFragment)
@ -31,6 +42,12 @@ interface GalleryInstallationComponent {
@Module
object GalleryInstallationModule {
@Provides
@PerScreen
fun provideGradientProvider(): SpaceGradientProvider {
return SpaceGradientProvider.Default
}
@Module
interface Declarations {
@ -44,5 +61,8 @@ object GalleryInstallationModule {
}
interface GalleryInstallationComponentDependencies : ComponentDependencies {
fun blockRepository(): BlockRepository
fun appCoroutineDispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
fun urlBuilder(): UrlBuilder
}

View file

@ -4,6 +4,7 @@ import android.net.Uri
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.misc.DeepLinkResolver
import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver
import timber.log.Timber
const val DEEP_LINK_PATTERN = "anytype://"
@ -12,22 +13,36 @@ const val IMPORT_PATH = "import"
const val INVITE_PATH = "invite"
const val TYPE_PARAM = "type"
const val SOURCE_PARAM = "source"
const val TYPE_VALUE_EXPERIENCE = "experience"
const val IMPORT_EXPERIENCE_DEEPLINK = "$DEEP_LINK_PATTERN$MAIN_PATH/$IMPORT_PATH?$TYPE_PARAM=$TYPE_VALUE_EXPERIENCE"
const val IMPORT_EXPERIENCE_DEEPLINK =
"$DEEP_LINK_PATTERN$MAIN_PATH/$IMPORT_PATH/?$TYPE_PARAM=$TYPE_VALUE_EXPERIENCE"
object DefaultDeepLinkResolver : DeepLinkResolver {
override fun resolve(
deeplink: String
): DeepLinkResolver.Action = when {
deeplink.contains(IMPORT_EXPERIENCE_DEEPLINK) -> {
DeepLinkResolver.Action.Import.Experience
try {
val type = Uri.parse(deeplink).getQueryParameter(TYPE_PARAM)
val source = Uri.parse(deeplink).getQueryParameter(SOURCE_PARAM)
DeepLinkResolver.Action.Import.Experience(
type = type.orEmpty(),
source = source.orEmpty()
)
} catch (e: Exception) {
Timber.e(e, "Error while resolving deeplink: $deeplink")
DeepLinkResolver.Action.Unknown
}
}
deeplink.contains(INVITE_PATH) -> {
DeepLinkResolver.Action.Invite(deeplink)
}
else -> DeepLinkResolver.Action.Unknown
}
}.also { Timber.d("Resolved deeplink: $deeplink to action: $it") }
}
object DefaultSpaceInviteResolver : SpaceInviteResolver {

View file

@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
@ -14,6 +15,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
@ -31,6 +33,9 @@ import javax.inject.Inject
class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
private val deepLinkType get() = argString(DEEPLINK_TYPE_KEY)
private val deepLinkSource get() = argString(DEEPLINK_SOURCE_KEY)
@Inject
lateinit var factory: GalleryInstallationViewModelFactory
private val vm by viewModels<GalleryInstallationViewModel> { factory }
@ -61,7 +66,9 @@ class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
GalleryInstallationNavigation.Main -> navController.navigate(
GalleryInstallationNavigation.Main.route
)
GalleryInstallationNavigation.Spaces -> navController.navigate(
GalleryInstallationNavigation.Spaces.route
)
GalleryInstallationNavigation.Dismiss -> navController.popBackStack()
else -> {}
}
@ -100,20 +107,42 @@ class GalleryInstallationFragment : BaseBottomSheetComposeFragment() {
skipCollapsed()
expand()
GalleryInstallationScreen(
state = vm.mainState.collectAsStateWithLifecycle().value
state = vm.mainState.collectAsStateWithLifecycle().value,
onInstallClicked = vm::onInstallClicked
)
}
@Composable
private fun InitSpacesScreen() {
GalleryInstallationSpacesScreen()
GalleryInstallationSpacesScreen(
state = vm.spacesViewState.collectAsStateWithLifecycle().value,
onNewSpaceClick = vm::onNewSpaceClick,
onSpaceClick = vm::onSpaceClick,
onDismiss = vm::onDismiss
)
}
override fun injectDependencies() {
componentManager().galleryInstallationsComponent.get().inject(this)
val params = GalleryInstallationViewModel.ViewModelParams(
deepLinkType = deepLinkType,
deepLinkSource = deepLinkSource
)
componentManager().galleryInstallationsComponent.get(params).inject(this)
}
override fun releaseDependencies() {
componentManager().galleryInstallationsComponent.release()
}
companion object {
const val DEEPLINK_TYPE_KEY = "arg.gallery-installation.deeplink-type-key"
const val DEEPLINK_SOURCE_KEY = "arg.gallery-installation.deeplink-source-key"
fun args(
deepLinkType: String,
deepLinkSource: String
): Bundle = bundleOf(
DEEPLINK_TYPE_KEY to deepLinkType,
DEEPLINK_SOURCE_KEY to deepLinkSource
)
}
}

View file

@ -30,6 +30,7 @@ import com.anytypeio.anytype.presentation.home.HomeScreenViewModel
import com.anytypeio.anytype.presentation.home.HomeScreenViewModel.Navigation
import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment
import com.anytypeio.anytype.ui.main.MainActivity
import com.anytypeio.anytype.ui.multiplayer.RequestJoinSpaceFragment
import com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment
@ -252,6 +253,15 @@ class HomeScreenFragment : BaseComposeFragment() {
RequestJoinSpaceFragment.args(link = command.link)
)
}
is Command.Deeplink.GalleryInstallation -> {
findNavController().navigate(
R.id.galleryInstallationScreen,
GalleryInstallationFragment.args(
deepLinkType = command.deepLinkType,
deepLinkSource = command.deepLinkSource
)
)
}
is Command.ShareSpace -> {
findNavController().navigate(
R.id.shareSpaceScreen,

View file

@ -279,6 +279,10 @@
android:id="@+id/paymentsScreen"
android:name="com.anytypeio.anytype.ui.payments.PaymentsFragment" />
<dialog
android:id="@+id/galleryInstallationScreen"
android:name="com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment" />
<fragment
android:id="@+id/splashScreen"
android:name="com.anytypeio.anytype.ui.splash.SplashFragment"

View file

@ -0,0 +1,47 @@
package com.anytypeio.anytype.other
import com.anytypeio.anytype.domain.misc.DeepLinkResolver
import org.junit.Assert.assertEquals
import org.junit.Test
class DefaultDeepLinkResolverTest {
private val deepLinkResolver = DefaultDeepLinkResolver
@Test
fun `resolve returns Import Experience for import experience deeplinks`() {
// Given
val deeplink = "anytype://main/import/?type=experience"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(DeepLinkResolver.Action.Import.Experience, result)
}
@Test
fun `resolve returns Invite with deeplink for invite deeplinks`() {
// Given
val deeplink = "anytype://invite/some_unique_identifier"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assert(result is DeepLinkResolver.Action.Invite)
assertEquals(deeplink, (result as DeepLinkResolver.Action.Invite).link)
}
@Test
fun `resolve returns Unknown for unrecognized deeplinks`() {
// Given
val deeplink = "anytype://some_random_path"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(DeepLinkResolver.Action.Unknown, result)
}
}

View file

@ -460,4 +460,13 @@ sealed class Command {
val inviteContentId: Id,
val inviteFileKey: String
)
data class DownloadGalleryManifest(val url: String)
data class ImportExperience(
val space: SpaceId,
val url: String,
val title: String,
val isNewSpace: Boolean
)
}

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.core_models
data class ManifestInfo(
val schema: String,
val id: String,
val name: String,
val author: String,
val license: String,
val title: String,
val description: String,
val screenshots: List<String>,
val downloadLink: String,
val fileSize: Int,
val categories: List<String>,
val language: String
)

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -964,4 +965,12 @@ class BlockDataRepository(
override suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink {
return remote.getSpaceInviteLink(spaceId)
}
override suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo? {
return remote.downloadGalleryManifest(command)
}
override suspend fun importExperience(command: Command.ImportExperience): Payload {
return remote.importExperience(command)
}
}

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -412,4 +413,7 @@ interface BlockRemote {
suspend fun stopSharingSpace(space: SpaceId)
suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink
suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo?
suspend fun importExperience(command: Command.ImportExperience): Payload
}

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -455,4 +456,7 @@ interface BlockRepository {
suspend fun stopSharingSpace(space: SpaceId)
suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink
suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo?
suspend fun importExperience(command: Command.ImportExperience): Payload
}

View file

@ -0,0 +1,23 @@
package com.anytypeio.anytype.domain.gallery_experience
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.ManifestInfo
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 DownloadGalleryManifest @Inject constructor(
dispatchers: AppCoroutineDispatchers,
private val repo: BlockRepository
) : ResultInteractor<DownloadGalleryManifest.Params, ManifestInfo?>(dispatchers.io) {
override suspend fun doWork(params: Params): ManifestInfo? {
val command = Command.DownloadGalleryManifest(
url = params.url
)
return repo.downloadGalleryManifest(command)
}
class Params(val url: String)
}

View file

@ -0,0 +1,32 @@
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
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import javax.inject.Inject
class ImportExperience @Inject constructor(
dispatchers: AppCoroutineDispatchers,
private val repo: BlockRepository
) : ResultInteractor<ImportExperience.Params, Payload>(dispatchers.io) {
override suspend fun doWork(params: Params): Payload {
val command = Command.ImportExperience(
space = params.spaceId,
url = params.url,
title = params.title,
isNewSpace = params.isNewSpace
)
return repo.importExperience(command)
}
data class Params(
val spaceId: SpaceId,
val url: String,
val title: String,
val isNewSpace: Boolean
)
}

View file

@ -7,7 +7,7 @@ interface DeepLinkResolver {
sealed class Action {
object Unknown : Action()
sealed class Import : Action() {
object Experience : Action()
data class Experience(val type: String, val source: String) : Action()
}
data class Invite(val link: String) : Action()
}

View file

@ -1,15 +1,29 @@
package com.anytypeio.anytype.gallery_experience.models
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
sealed class GalleryInstallationState {
object Hidden : GalleryInstallationState()
object Loading : GalleryInstallationState()
object Success : GalleryInstallationState()
data class Success(val info: ManifestInfo) : GalleryInstallationState()
}
data class GalleryInstallationSpacesState(
val spaces: List<GallerySpaceView>,
val isNewButtonVisible: Boolean
)
sealed class GalleryInstallationNavigation(val route: String) {
object Main : GalleryInstallationNavigation("main")
object Spaces : GalleryInstallationNavigation("spaces")
object Success : GalleryInstallationNavigation("success")
object Error : GalleryInstallationNavigation("error")
object Dismiss : GalleryInstallationNavigation("")
}
}
data class GallerySpaceView(
val obj: ObjectWrapper.SpaceView,
val icon: SpaceIconView
)

View file

@ -1,16 +1,11 @@
package com.anytypeio.anytype.gallery_experience.screens
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationState
@Composable
fun GalleryInstallationScreen(state: GalleryInstallationState) {
}
@Preview
@Composable
private fun GalleryInstallationScreenPreview() {
GalleryInstallationScreen(GalleryInstallationState.Success)
fun GalleryInstallationScreen(
state: GalleryInstallationState,
onInstallClicked: () -> Unit
) {
}

View file

@ -1,14 +1,17 @@
package com.anytypeio.anytype.gallery_experience.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationSpacesState
import com.anytypeio.anytype.gallery_experience.models.GallerySpaceView
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GalleryInstallationSpacesScreen() {
}
fun GalleryInstallationSpacesScreen(
state: GalleryInstallationSpacesState,
onNewSpaceClick: () -> Unit,
onSpaceClick: (GallerySpaceView) -> Unit,
onDismiss: () -> Unit
) {
@Preview
@Composable
private fun GallerySpacesScreenPreview() {
GalleryInstallationSpacesScreen()
}

View file

@ -1,20 +1,165 @@
package com.anytypeio.anytype.gallery_experience.viewmodel
import androidx.lifecycle.ViewModel
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.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.spaces.CreateSpace
import com.anytypeio.anytype.domain.spaces.GetSpaceViews
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationNavigation
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationSpacesState
import com.anytypeio.anytype.gallery_experience.models.GalleryInstallationState
import com.anytypeio.anytype.gallery_experience.models.GallerySpaceView
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.presentation.spaces.spaceIcon
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class GalleryInstallationViewModel(
private val viewModelParams: ViewModelParams,
private val downloadGalleryManifest: DownloadGalleryManifest,
private val importExperience: ImportExperience,
private val analytics: Analytics,
private val getSpaceViews: GetSpaceViews,
private val createSpace: CreateSpace,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider
) : ViewModel() {
val mainState = MutableStateFlow<GalleryInstallationState>(GalleryInstallationState.Hidden)
val mainState = MutableStateFlow<GalleryInstallationState>(GalleryInstallationState.Loading)
val spacesViewState =
MutableStateFlow(GalleryInstallationSpacesState(emptyList(), false))
val command = MutableStateFlow<GalleryInstallationNavigation?>(null)
init {
Timber.d("GalleryInstallationViewModel init")
Timber.d("GalleryInstallationViewModel init, viewModelParams: $viewModelParams")
downloadGalleryManifest()
}
}
private fun downloadGalleryManifest() {
viewModelScope.launch {
val params = DownloadGalleryManifest.Params(url = viewModelParams.deepLinkSource)
downloadGalleryManifest.async(params).fold(
onSuccess = { manifestInfo ->
if (manifestInfo != null) {
Timber.d("DownloadGalleryManifest success, manifestInfo: $manifestInfo")
mainState.value = GalleryInstallationState.Success(manifestInfo)
} else {
Timber.e("DownloadGalleryManifest failed, manifestInfo is null")
command.value = GalleryInstallationNavigation.Error
}
},
onFailure = { error ->
Timber.e(error, "DownloadGalleryManifest failed")
command.value = GalleryInstallationNavigation.Dismiss
}
)
}
}
fun onInstallClicked() {
viewModelScope.launch {
getSpaceViews.async(Unit).fold(
onSuccess = { spaces ->
Timber.d("GetSpaceViews success, spaceViews: $spaces")
spacesViewState.value = GalleryInstallationSpacesState(
spaces = spaces.map {
it.toView(urlBuilder, spaceGradientProvider)
},
isNewButtonVisible = true
)
command.value = GalleryInstallationNavigation.Spaces
},
onFailure = { error ->
Timber.e(error, "GetSpaceViews failed")
}
)
}
}
fun onNewSpaceClick() {
command.value = GalleryInstallationNavigation.Dismiss
val manifestInfo = (mainState.value as? GalleryInstallationState.Success)?.info ?: return
val params = CreateSpace.Params(
details = mapOf(
Relations.NAME to manifestInfo.name,
Relations.ICON_OPTION to spaceGradientProvider.randomId().toDouble()
)
)
viewModelScope.launch {
createSpace.async(params).fold(
onSuccess = { space ->
Timber.d("CreateSpace success, space: $space")
proceedWithInstallation(
spaceId = SpaceId(space),
isNewSpace = true,
manifestInfo = manifestInfo
)
},
onFailure = { error ->
Timber.e(error, "CreateSpace failed")
}
)
}
}
fun onSpaceClick(space: GallerySpaceView) {
command.value = GalleryInstallationNavigation.Dismiss
val manifestInfo = (mainState.value as? GalleryInstallationState.Success)?.info ?: return
proceedWithInstallation(
spaceId = SpaceId(space.obj.id),
isNewSpace = false,
manifestInfo = manifestInfo
)
}
fun onDismiss() {
command.value = GalleryInstallationNavigation.Dismiss
}
private fun proceedWithInstallation(
spaceId: SpaceId,
isNewSpace: Boolean,
manifestInfo: ManifestInfo
) {
val params = ImportExperience.Params(
spaceId = spaceId,
url = manifestInfo.downloadLink,
title = manifestInfo.title,
isNewSpace = isNewSpace
)
viewModelScope.launch {
importExperience.async(params).fold(
onSuccess = {
Timber.d("ObjectImportExperience success")
command.value = GalleryInstallationNavigation.Success
},
onFailure = { error ->
Timber.e(error, "ObjectImportExperience failed")
command.value = GalleryInstallationNavigation.Error
}
)
}
}
data class ViewModelParams(
val deepLinkType: String,
val deepLinkSource: String
)
}
private fun ObjectWrapper.SpaceView.toView(
builder: UrlBuilder,
spaceGradientProvider: SpaceGradientProvider
) = GallerySpaceView(
obj = this,
icon = spaceIcon(builder, spaceGradientProvider)
)

View file

@ -3,15 +3,35 @@ package com.anytypeio.anytype.gallery_experience.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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.spaces.CreateSpace
import com.anytypeio.anytype.domain.spaces.GetSpaceViews
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
class GalleryInstallationViewModelFactory@Inject constructor(
class GalleryInstallationViewModelFactory @Inject constructor(
private val viewModelParams: GalleryInstallationViewModel.ViewModelParams,
private val downloadGalleryManifest: DownloadGalleryManifest,
private val importExperience: ImportExperience,
private val analytics: Analytics,
private val getSpaceViews: GetSpaceViews,
private val createSpace: CreateSpace,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GalleryInstallationViewModel(
viewModelParams = viewModelParams,
downloadGalleryManifest = downloadGalleryManifest,
importExperience = importExperience,
analytics = analytics,
getSpaceViews = getSpaceViews,
urlBuilder = urlBuilder,
spaceGradientProvider = spaceGradientProvider,
createSpace = createSpace
) as T
}
}

View file

@ -1434,4 +1434,10 @@
<string name="payments_welcome_subtitle">Big cheers for your curiosity!</string>
<string name="payments_welcome_button">Continue</string>
<!-- Gallery Experience -->
<string name="gallery_experience_made">Made by</string>
<string name="gallery_experience_install">Install</string>
<string name="gallery_experience_install_new">Install to new space</string>
</resources>

View file

@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -926,4 +927,12 @@ class BlockMiddleware(
override suspend fun getSpaceInviteLink(spaceId: SpaceId): SpaceInviteLink {
return middleware.getCurrentSpaceInvite(spaceId)
}
override suspend fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo? {
return middleware.downloadGalleryManifest(command)
}
override suspend fun importExperience(command: Command.ImportExperience): Payload {
return middleware.importExperience(command)
}
}

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -2502,6 +2503,33 @@ class Middleware @Inject constructor(
)
}
@Throws(Exception::class)
fun downloadGalleryManifest(command: Command.DownloadGalleryManifest): ManifestInfo? {
val request = Rpc.Gallery.DownloadManifest.Request(
url = command.url
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.downloadManifest(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.info?.toCoreModel()
}
@Throws(Exception::class)
fun importExperience(
command: Command.ImportExperience
): Payload {
val request = Rpc.Object.ImportExperience.Request(
spaceId = command.space.id,
url = command.url,
title = command.title,
isNewSpace = command.isNewSpace
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.objectImportExperience(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.event.toPayload()
}
private fun logRequest(any: Any) {
logger.logRequest(any).also {
if (BuildConfig.DEBUG && threadInfo.isOnMainThread()) {

View file

@ -68,3 +68,5 @@ typealias MWidgetLayout = anytype.model.Block.Content.Widget.Layout
typealias MNetworkMode = anytype.Rpc.Account.NetworkMode
typealias MParticipantPermission = anytype.model.ParticipantPermissions
typealias MManifestInfo = anytype.model.ManifestInfo

View file

@ -23,6 +23,7 @@ import com.anytypeio.anytype.core_models.DVViewerRelation
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ManifestInfo
import com.anytypeio.anytype.core_models.NodeUsage
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectOrder
@ -789,4 +790,21 @@ fun Account.Info.config() : Config = Config(
analytics = analyticsId,
device = deviceId,
network = networkId
)
)
fun MManifestInfo.toCoreModel(): ManifestInfo {
return ManifestInfo(
schema = schema,
id = id,
name = name,
author = author,
license = license,
title = title,
description = description,
screenshots = screenshots,
downloadLink = downloadLink,
fileSize = fileSize,
categories = categories,
language = language
)
}

View file

@ -522,4 +522,12 @@ interface MiddlewareService {
fun spaceStopSharing(request: Rpc.Space.StopSharing.Request): Rpc.Space.StopSharing.Response
//endregion
//region GALLERY EXPERIENCE
@Throws(Exception::class)
fun downloadManifest(request: Rpc.Gallery.DownloadManifest.Request): Rpc.Gallery.DownloadManifest.Response
@Throws(Exception::class)
fun objectImportExperience(request: Rpc.Object.ImportExperience.Request): Rpc.Object.ImportExperience.Response
//endregion
}

View file

@ -1919,4 +1919,30 @@ class MiddlewareServiceImplementation @Inject constructor(
return response
}
}
override fun downloadManifest(request: Rpc.Gallery.DownloadManifest.Request): Rpc.Gallery.DownloadManifest.Response {
val encoded = Service.galleryDownloadManifest(
Rpc.Gallery.DownloadManifest.Request.ADAPTER.encode(request)
)
val response = Rpc.Gallery.DownloadManifest.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Gallery.DownloadManifest.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun objectImportExperience(request: Rpc.Object.ImportExperience.Request): Rpc.Object.ImportExperience.Response {
val encoded = Service.objectImportExperience(
Rpc.Object.ImportExperience.Request.ADAPTER.encode(request)
)
val response = Rpc.Object.ImportExperience.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Object.ImportExperience.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
}

View file

@ -1047,13 +1047,18 @@ class HomeScreenViewModel(
fun onResume(deeplink: DeepLinkResolver.Action? = null) {
Timber.d("onResume, deeplink: ${deeplink}")
when(deeplink) {
when (deeplink) {
is DeepLinkResolver.Action.Import.Experience -> {
viewModelScope.launch {
delay(1000)
commands.emit(Command.Deeplink.CannotImportExperience)
commands.emit(
Command.Deeplink.GalleryInstallation(
deepLinkType = deeplink.type,
deepLinkSource = deeplink.source
)
)
}
}
is DeepLinkResolver.Action.Invite -> {
viewModelScope.launch {
delay(1000)
@ -1574,6 +1579,10 @@ sealed class Command {
sealed class Deeplink : Command() {
object CannotImportExperience : Deeplink()
data class Invite(val link: String) : Deeplink()
data class GalleryInstallation(
val deepLinkType: String,
val deepLinkSource: String
) : Deeplink()
}
}