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

DROID-2731 Vault | Tech | Basic behavior (space creation, space switching) (#1516)

This commit is contained in:
Evgenii Kozlov 2024-08-29 12:38:23 +02:00 committed by GitHub
parent b0fc366d7f
commit 6bb9ef1716
Signed by: github
GPG key ID: B5690EEEBB952194
13 changed files with 325 additions and 30 deletions

View file

@ -6,7 +6,10 @@ 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.config.UserSettingsRepository
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.vault.VaultViewModel
import com.anytypeio.anytype.ui.vault.VaultFragment
import dagger.Binds
@ -48,4 +51,7 @@ interface VaultComponentDependencies : ComponentDependencies {
fun appCoroutineDispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
fun urlBuilder(): UrlBuilder
fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer
fun userSettingsRepository(): UserSettingsRepository
fun spaceManager(): SpaceManager
}

View file

@ -18,6 +18,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavOptions
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.extensions.throttledClick
@ -107,7 +108,11 @@ class HomeScreenFragment : BaseComposeFragment() {
onProfileClicked = throttledClick(
onClick = {
runCatching {
findNavController().navigate(R.id.action_open_spaces)
if (BuildConfig.DEBUG) {
findNavController().navigate(R.id.action_open_vault)
} else {
findNavController().navigate(R.id.action_open_spaces)
}
}
}
),

View file

@ -4,13 +4,21 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.vault.VaultViewModel
import com.anytypeio.anytype.presentation.vault.VaultViewModel.Command
import com.anytypeio.anytype.ui.settings.ProfileSettingsFragment
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
class VaultFragment : BaseComposeFragment() {
@ -27,18 +35,51 @@ class VaultFragment : BaseComposeFragment() {
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
VaultScreen(
spaces = vm.spaces.collectAsStateWithLifecycle().value,
onSpaceClicked = {
// TODO
},
onCreateSpaceClicked = {
// TODO
MaterialTheme(typography = typography) {
VaultScreen(
spaces = vm.spaces.collectAsStateWithLifecycle().value,
onSpaceClicked = vm::onSpaceClicked,
onCreateSpaceClicked = vm::onCreateSpaceClicked,
onSettingsClicked = vm::onSettingsClicked
)
}
LaunchedEffect(Unit) {
vm.commands.collect { command ->
proceedWithCommand(command)
}
)
}
}
}
private fun proceedWithCommand(command: Command) {
when (command) {
is Command.EnterSpaceHomeScreen -> {
runCatching {
findNavController().popBackStack()
}
}
is Command.CreateNewSpace -> {
runCatching {
findNavController().navigate(
R.id.createSpaceScreen
)
}
}
is Command.OpenProfileSettings -> {
runCatching {
findNavController().navigate(
R.id.profileScreen,
bundleOf(ProfileSettingsFragment.SPACE_ID_KEY to command.space.id)
)
}
}
}
}
override fun onApplyWindowRootInsets(view: View) {
// TODO Do nothing ?
}
override fun injectDependencies() {
componentManager().vaultComponent.get().inject(this)
}

View file

@ -1,17 +1,22 @@
package com.anytypeio.anytype.ui.vault
import android.content.res.Configuration
import android.os.Build.VERSION.SDK_INT
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@ -20,6 +25,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -27,37 +34,56 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.BuildConfig.USE_EDGE_TO_EDGE
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyBold
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.presentation.editor.cover.CoverGradient
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.vault.VaultViewModel.VaultSpaceView
import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor
import com.anytypeio.anytype.ui.widgets.types.gradient
@Composable
fun VaultScreen(
spaces: List<VaultSpaceView>,
onSpaceClicked: (VaultSpaceView) -> Unit,
onCreateSpaceClicked: () -> Unit
onCreateSpaceClicked: () -> Unit,
onSettingsClicked: () -> Unit
) {
Box(
Modifier.fillMaxSize()
Modifier
.fillMaxSize()
.background(
color = colorResource(id = R.color.background_primary)
)
.then(
if (USE_EDGE_TO_EDGE && SDK_INT >= EDGE_TO_EDGE_MIN_SDK)
Modifier.windowInsetsPadding(WindowInsets.systemBars)
else
Modifier
)
) {
VaultScreenToolbar(
onPlusClicked = onCreateSpaceClicked
onPlusClicked = onCreateSpaceClicked,
onSettingsClicked = onSettingsClicked
)
LazyColumn(
Modifier
modifier = Modifier
.fillMaxSize()
.padding(
top = 48.dp
)
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = spaces,
@ -73,9 +99,8 @@ fun VaultScreen(
SpaceAccessType.SHARED -> stringResource(id = R.string.space_type_shared_space)
else -> EMPTY_STRING_VALUE
},
onCardClicked = {
onSpaceClicked(item)
}
wallpaper = item.wallpaper,
onCardClicked = { onSpaceClicked(item) }
)
}
}
@ -85,7 +110,8 @@ fun VaultScreen(
@Composable
fun VaultScreenToolbar(
onPlusClicked: () -> Unit
onPlusClicked: () -> Unit,
onSettingsClicked: () -> Unit
) {
Box(
modifier = Modifier
@ -99,11 +125,14 @@ fun VaultScreenToolbar(
modifier = Modifier.align(Alignment.Center)
)
Image(
painter = painterResource(id = R.drawable.ic_space_settings),
painter = painterResource(id = R.drawable.ic_vault_settings),
contentDescription = "Settings icon",
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp)
.noRippleClickable {
onSettingsClicked()
}
)
Image(
// TODO change icon
@ -123,13 +152,49 @@ fun VaultScreenToolbar(
fun VaultSpaceCard(
title: String,
subtitle: String,
onCardClicked: () -> Unit
onCardClicked: () -> Unit,
wallpaper: Wallpaper
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
.padding(horizontal = 8.dp)
.clip(RoundedCornerShape(20.dp))
.then(
when(wallpaper) {
is Wallpaper.Color -> {
val color = WallpaperColor.entries.find {
it.code == wallpaper.code
}
if (color != null) {
Modifier.background(
color = Color(Integer.decode(color.hex)),
shape = RoundedCornerShape(20.dp)
)
} else {
Modifier
}
}
is Wallpaper.Gradient -> {
Modifier.background(
brush = Brush.horizontalGradient(
colors = gradient(wallpaper.code)
),
shape = RoundedCornerShape(20.dp)
)
}
is Wallpaper.Default -> {
Modifier.background(
brush = Brush.horizontalGradient(
colors = gradient(CoverGradient.SKY)
),
shape = RoundedCornerShape(20.dp)
)
}
else -> Modifier
}
)
.clickable {
onCardClicked()
}
@ -137,13 +202,13 @@ fun VaultSpaceCard(
// TODO render space icon
Box(
modifier = Modifier
.padding(start = 16.dp)
.size(64.dp)
.background(
color = Color.Red,
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.align(Alignment.CenterStart)
.padding(start = 16.dp)
)
Column(
modifier = Modifier
@ -177,7 +242,8 @@ fun VaultSpaceCard(
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
fun VaultScreenToolbarPreview() {
VaultScreenToolbar(
onPlusClicked = {}
onPlusClicked = {},
onSettingsClicked = {}
)
}
@ -188,7 +254,8 @@ fun VaultSpaceCardPreview() {
VaultSpaceCard(
title = "B&O Museum",
subtitle = "Private space",
onCardClicked = {}
onCardClicked = {},
wallpaper = Wallpaper.Default
)
}
@ -211,6 +278,7 @@ fun VaultScreenPreview() {
)
},
onSpaceClicked = {},
onCreateSpaceClicked = {}
onCreateSpaceClicked = {},
onSettingsClicked = {}
)
}

View file

@ -158,6 +158,9 @@
<action
android:id="@+id/action_open_spaces"
app:destination="@id/selectSpaceScreen" />
<action
android:id="@+id/action_open_vault"
app:destination="@id/vaultScreen" />
</fragment>
<fragment
@ -191,6 +194,10 @@
app:enterAnim="@anim/anim_switch_space_enter" />
</dialog>
<fragment
android:id="@+id/vaultScreen"
android:name="com.anytypeio.anytype.ui.vault.VaultFragment"/>
<dialog
android:id="@+id/createSpaceScreen"
android:name="com.anytypeio.anytype.ui.spaces.CreateSpaceFragment">

View file

@ -1,7 +1,7 @@
package com.anytypeio.anytype.core_models
sealed class Wallpaper {
object Default: Wallpaper()
data object Default: Wallpaper()
data class Color(val code: Id) : Wallpaper()
data class Gradient(val code: Id) : Wallpaper()
data class Image(val hash: Hash) : Wallpaper()

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M10.25,0.75C10.25,0.336 10.586,0 11,0C11.414,0 11.75,0.336 11.75,0.75V2.533C12.755,2.621 13.711,2.883 14.585,3.291L15.476,1.748C15.683,1.39 16.141,1.267 16.5,1.474C16.796,1.645 16.932,1.988 16.853,2.304C16.837,2.371 16.81,2.436 16.774,2.499L15.883,4.042C16.689,4.608 17.392,5.311 17.958,6.117L19.502,5.225C19.861,5.018 20.319,5.141 20.526,5.5C20.733,5.859 20.611,6.317 20.252,6.524L18.709,7.415C19.117,8.289 19.379,9.245 19.467,10.25H21.25C21.664,10.25 22,10.586 22,11C22,11.414 21.664,11.75 21.25,11.75H19.467C19.379,12.755 19.117,13.711 18.709,14.585L20.252,15.476C20.611,15.683 20.733,16.141 20.526,16.5C20.319,16.859 19.861,16.982 19.502,16.775L17.958,15.883C17.392,16.689 16.689,17.392 15.883,17.958L16.774,19.502C16.852,19.636 16.883,19.785 16.873,19.929C16.86,20.115 16.778,20.293 16.637,20.425C16.596,20.463 16.551,20.497 16.5,20.526C16.141,20.733 15.683,20.61 15.476,20.252L14.585,18.709C13.711,19.117 12.755,19.379 11.75,19.467V21.25C11.75,21.664 11.414,22 11,22C10.586,22 10.25,21.664 10.25,21.25V19.467C9.245,19.379 8.289,19.117 7.415,18.709L6.524,20.252C6.317,20.611 5.859,20.733 5.5,20.526C5.141,20.319 5.018,19.861 5.225,19.502L6.117,17.958C5.311,17.392 4.608,16.689 4.042,15.883L2.498,16.774C2.139,16.982 1.681,16.859 1.474,16.5C1.267,16.141 1.389,15.682 1.748,15.475L3.291,14.585C2.883,13.71 2.621,12.755 2.533,11.75H0.75C0.724,11.75 0.699,11.749 0.673,11.746C0.295,11.708 0,11.388 0,11C0,10.586 0.336,10.25 0.75,10.25H2.533C2.621,9.245 2.883,8.29 3.291,7.415L1.748,6.525C1.389,6.318 1.267,5.859 1.474,5.5C1.681,5.141 2.139,5.018 2.498,5.226L4.042,6.117C4.608,5.311 5.311,4.608 6.117,4.042L5.225,2.498C5.018,2.139 5.141,1.681 5.5,1.474C5.859,1.267 6.317,1.389 6.524,1.748L7.415,3.291C8.289,2.883 9.245,2.621 10.25,2.533V0.75ZM4.04,11.75C4.414,15.263 7.387,18 11,18C12.008,18 12.966,17.787 13.831,17.404L10.567,11.75H4.04ZM10.567,10.25H4.04C4.414,6.737 7.387,4 11,4C12.008,4 12.966,4.213 13.831,4.596L10.567,10.25ZM15.13,16.653L11.866,11L15.13,5.347C16.87,6.621 18,8.678 18,11C18,13.322 16.87,15.379 15.13,16.653Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -23,6 +23,7 @@ interface UserSettingsCache {
suspend fun setWallpaper(space: Id, wallpaper: Wallpaper)
suspend fun getWallpaper(space: Id) : Wallpaper
suspend fun getWallpapers(): Map<Id, Wallpaper>
suspend fun setThemeMode(mode: ThemeMode)
suspend fun getThemeMode(): ThemeMode
suspend fun getWidgetSession() : WidgetSession

View file

@ -17,6 +17,10 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
override suspend fun getWallpaper(space: Id): Wallpaper = cache.getWallpaper(space)
override suspend fun getWallpapers(): Map<Id, Wallpaper> {
return cache.getWallpapers()
}
override suspend fun setDefaultObjectType(
space: SpaceId,
type: TypeId

View file

@ -15,6 +15,7 @@ interface UserSettingsRepository {
suspend fun setWallpaper(space: Id, wallpaper: Wallpaper)
suspend fun getWallpaper(space: Id): Wallpaper
suspend fun getWallpapers(): Map<Id, Wallpaper>
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
suspend fun getDefaultObjectType(space: SpaceId): TypeId?

View file

@ -0,0 +1,18 @@
package com.anytypeio.anytype.domain.wallpaper
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
class GetSpaceWallpapers @Inject constructor(
private val repo: UserSettingsRepository,
private val dispatchers: AppCoroutineDispatchers
) : ResultInteractor<Unit, Map<Id, Wallpaper>>(dispatchers.io) {
override suspend fun doWork(params: Unit): Map<Id, Wallpaper> {
return repo.getWallpapers()
}
}

View file

@ -137,6 +137,18 @@ class DefaultUserSettingsCache(
}
}
override suspend fun getWallpapers(): Map<Id, Wallpaper> {
val rawSettings = prefs.getString(WALLPAPER_SETTINGS_KEY, "")
return if (rawSettings.isNullOrEmpty()) {
emptyMap()
} else {
val deserialized = rawSettings.deserializeWallpaperSettings()
return deserialized.mapValues { setting ->
setting.value.asWallpaper()
}
}
}
override suspend fun setThemeMode(mode: ThemeMode) {
prefs
.edit()

View file

@ -2,32 +2,154 @@ package com.anytypeio.anytype.presentation.vault
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Wallpaper
import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.spaces.SaveCurrentSpace
import com.anytypeio.anytype.domain.wallpaper.GetSpaceWallpapers
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.spaces.spaceIcon
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
class VaultViewModel() : BaseViewModel() {
class VaultViewModel(
private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer,
private val urlBuilder: UrlBuilder,
private val getSpaceWallpapers: GetSpaceWallpapers,
private val spaceManager: SpaceManager,
private val saveCurrentSpace: SaveCurrentSpace,
) : BaseViewModel() {
val spaces = MutableStateFlow<List<VaultSpaceView>>(emptyList())
val commands = MutableSharedFlow<Command>(replay = 0)
init {
Timber.i("VaultViewModel, init")
viewModelScope.launch {
val wallpapers = getSpaceWallpapers.async(Unit).getOrNull() ?: emptyMap()
spaceViewSubscriptionContainer
.observe()
.map { spaces ->
spaces
.filter { space ->
space.spaceLocalStatus == SpaceStatus.OK
&& !space.spaceAccountStatus.isDeletedOrRemoving()
}
.map { space ->
VaultSpaceView(
space = space,
icon = space.spaceIcon(
builder = urlBuilder,
spaceGradientProvider = SpaceGradientProvider.Default
),
wallpaper = wallpapers.getOrDefault(
key = space.targetSpaceId,
defaultValue = Wallpaper.Default
)
)
}
}.collect {
spaces.value = it
}
}
}
fun onSpaceClicked(view: VaultSpaceView) {
Timber.i("onSpaceClicked")
viewModelScope.launch {
val targetSpace = view.space.targetSpaceId
if (targetSpace != null) {
spaceManager.set(targetSpace).fold(
onFailure = {
Timber.e(it, "Could not select space")
},
onSuccess = {
proceedWithSavingCurrentSpace(targetSpace)
}
)
} else {
Timber.e("Missing target space")
}
}
}
fun onSettingsClicked() {
viewModelScope.launch {
val entrySpaceView = spaces.value.find { space ->
space.space.spaceAccessType == SpaceAccessType.DEFAULT
}
if (entrySpaceView != null && entrySpaceView.space.targetSpaceId != null) {
commands.emit(
Command.OpenProfileSettings(
space = SpaceId(requireNotNull(entrySpaceView.space.targetSpaceId))
)
)
} else {
Timber.w("Entry space not found")
}
}
}
fun onCreateSpaceClicked() {
viewModelScope.launch {
commands.emit(Command.CreateNewSpace)
}
}
private suspend fun proceedWithSavingCurrentSpace(targetSpace: String) {
saveCurrentSpace.async(
SaveCurrentSpace.Params(SpaceId(targetSpace))
).fold(
onFailure = {
Timber.e(it, "Error while saving current space on vault screen")
},
onSuccess = {
commands.emit(Command.EnterSpaceHomeScreen)
}
)
}
class Factory @Inject constructor(
private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer,
private val getSpaceWallpapers: GetSpaceWallpapers,
private val urlBuilder: UrlBuilder,
private val spaceManager: SpaceManager,
private val saveCurrentSpace: SaveCurrentSpace,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>
) = VaultViewModel() as T
) = VaultViewModel(
spaceViewSubscriptionContainer = spaceViewSubscriptionContainer,
getSpaceWallpapers = getSpaceWallpapers,
urlBuilder = urlBuilder,
spaceManager = spaceManager,
saveCurrentSpace = saveCurrentSpace
) as T
}
data class VaultSpaceView(
val space: ObjectWrapper.SpaceView,
val icon: SpaceIconView
val icon: SpaceIconView,
val wallpaper: Wallpaper = Wallpaper.Default
)
sealed class Command {
data object EnterSpaceHomeScreen: Command()
data object CreateNewSpace: Command()
data class OpenProfileSettings(val space: SpaceId): Command()
}
}