DROID-2146 Objects | Enhancement | Allow pinning and unpinning types in the new object-creation panel (#771)
This commit is contained in:
parent
60415edafd
commit
e6acc965ff
|
@ -6,6 +6,7 @@ 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.workspace.SpaceManager
|
||||
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
|
||||
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
|
||||
|
@ -48,4 +49,5 @@ interface SelectObjectTypeDependencies : ComponentDependencies {
|
|||
fun analytics(): Analytics
|
||||
fun dispatchers(): AppCoroutineDispatchers
|
||||
fun spaceManager(): SpaceManager
|
||||
fun userSettingsRepo(): UserSettingsRepository
|
||||
}
|
|
@ -277,7 +277,11 @@ object DataModule {
|
|||
@Singleton
|
||||
fun provideUserSettingsCache(
|
||||
@Named("default") prefs: SharedPreferences,
|
||||
): UserSettingsCache = DefaultUserSettingsCache(prefs)
|
||||
context: Context
|
||||
): UserSettingsCache = DefaultUserSettingsCache(
|
||||
prefs = prefs,
|
||||
context = context
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
|
|||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
|
@ -23,6 +24,8 @@ import com.anytypeio.anytype.presentation.objects.Command
|
|||
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
|
||||
import com.anytypeio.anytype.ui.settings.typography
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
||||
|
||||
|
@ -48,6 +51,20 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
|||
SelectObjectTypeScreen(
|
||||
state = vm.viewState.collectAsStateWithLifecycle().value,
|
||||
onTypeClicked = vm::onTypeClicked,
|
||||
onPinOnTopClicked = {
|
||||
lifecycleScope.launch {
|
||||
// Workaround to prevent dropdown-menu flickering
|
||||
delay(DROP_DOWN_MENU_ACTION_DELAY)
|
||||
vm.onPinTypeClicked(it)
|
||||
}
|
||||
},
|
||||
onUnpinTypeClicked = {
|
||||
lifecycleScope.launch {
|
||||
// Workaround to prevent dropdown-menu flickering
|
||||
delay(DROP_DOWN_MENU_ACTION_DELAY)
|
||||
vm.onUnpinTypeClicked(it)
|
||||
}
|
||||
},
|
||||
onQueryChanged = vm::onQueryChanged,
|
||||
onFocused = {
|
||||
skipCollapsed()
|
||||
|
@ -89,6 +106,7 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
|||
|
||||
companion object {
|
||||
const val EXCLUDED_TYPE_KEYS_ARG_KEY = "arg.create-object-of-type.excluded-type-keys"
|
||||
const val DROP_DOWN_MENU_ACTION_DELAY = 100L
|
||||
|
||||
fun newInstance(
|
||||
excludedTypeKeys: List<Key>,
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -32,6 +32,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
|
@ -55,6 +57,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.anytypeio.anytype.R
|
||||
|
@ -70,6 +73,7 @@ import com.anytypeio.anytype.core_ui.views.Title2
|
|||
import com.anytypeio.anytype.emojifier.Emojifier
|
||||
import com.anytypeio.anytype.presentation.objects.SelectTypeView
|
||||
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -78,13 +82,17 @@ fun PreviewScreen() {
|
|||
onTypeClicked = {},
|
||||
state = SelectTypeViewState.Loading,
|
||||
onQueryChanged = {},
|
||||
onFocused = {}
|
||||
onFocused = {},
|
||||
onUnpinTypeClicked = {},
|
||||
onPinOnTopClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectObjectTypeScreen(
|
||||
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onPinOnTopClicked: (SelectTypeView.Type) -> Unit,
|
||||
onQueryChanged: (String) -> Unit,
|
||||
onFocused: () -> Unit,
|
||||
state: SelectTypeViewState
|
||||
|
@ -104,18 +112,30 @@ fun SelectObjectTypeScreen(
|
|||
onFocused = onFocused
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
ScreenContent(state, onTypeClicked)
|
||||
ScreenContent(
|
||||
state = state,
|
||||
onTypeClicked = onTypeClicked,
|
||||
onPinOnTopClicked = onPinOnTopClicked,
|
||||
onUnpinTypeClicked = onUnpinTypeClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
state: SelectTypeViewState,
|
||||
onTypeClicked: (SelectTypeView.Type) -> Unit
|
||||
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
|
||||
) {
|
||||
when (state) {
|
||||
is SelectTypeViewState.Content -> {
|
||||
FlowRowContent(state.views, onTypeClicked)
|
||||
FlowRowContent(
|
||||
views = state.views,
|
||||
onTypeClicked = onTypeClicked,
|
||||
onPinOnTopClicked = onPinOnTopClicked,
|
||||
onUnpinTypeClicked = onUnpinTypeClicked
|
||||
)
|
||||
}
|
||||
SelectTypeViewState.Empty -> {
|
||||
AnimatedVisibility(
|
||||
|
@ -149,7 +169,9 @@ private fun ScreenContent(
|
|||
@OptIn(ExperimentalLayoutApi::class)
|
||||
private fun FlowRowContent(
|
||||
views: List<SelectTypeView>,
|
||||
onTypeClicked: (SelectTypeView.Type) -> Unit
|
||||
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
|
@ -162,13 +184,62 @@ private fun FlowRowContent(
|
|||
views.forEach { view ->
|
||||
when (view) {
|
||||
is SelectTypeView.Type -> {
|
||||
ObjectTypeItem(
|
||||
name = view.name,
|
||||
emoji = view.icon,
|
||||
onItemClicked = throttledClick(
|
||||
onClick = { onTypeClicked(view) }
|
||||
),
|
||||
modifier = Modifier
|
||||
val isMenuExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
Box {
|
||||
ObjectTypeItem(
|
||||
name = view.name,
|
||||
emoji = view.icon,
|
||||
onItemClicked = throttledClick(
|
||||
onClick = { onTypeClicked(view) }
|
||||
),
|
||||
onItemLongClicked = {
|
||||
isMenuExpanded.value = !isMenuExpanded.value
|
||||
},
|
||||
modifier = Modifier
|
||||
)
|
||||
if (view.isPinnable) {
|
||||
DropdownMenu(
|
||||
expanded = isMenuExpanded.value,
|
||||
onDismissRequest = { isMenuExpanded.value = false },
|
||||
offset = DpOffset(x = 0.dp, y = 6.dp)
|
||||
) {
|
||||
if (!view.isPinned || !view.isFirstInSection) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
isMenuExpanded.value = false
|
||||
onPinOnTopClicked(view)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.any_object_creation_menu_pin_on_top),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (view.isPinned) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
isMenuExpanded.value = false
|
||||
onUnpinTypeClicked(view)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.any_object_creation_menu_unpin),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is SelectTypeView.Section.Pinned -> {
|
||||
Section(
|
||||
title = stringResource(id = R.string.create_object_section_pinned),
|
||||
)
|
||||
}
|
||||
is SelectTypeView.Section.Groups -> {
|
||||
|
@ -230,6 +301,16 @@ private fun LazyColumnContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
is SelectTypeView.Section.Pinned -> {
|
||||
item(
|
||||
key = view.javaClass.name,
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
) {
|
||||
Section(
|
||||
title = stringResource(id = R.string.create_object_section_pinned)
|
||||
)
|
||||
}
|
||||
}
|
||||
is SelectTypeView.Section.Library -> {
|
||||
item(
|
||||
key = view.javaClass.name,
|
||||
|
@ -252,6 +333,9 @@ private fun LazyColumnContent(
|
|||
onTypeClicked(view)
|
||||
}
|
||||
),
|
||||
onItemLongClicked = {
|
||||
|
||||
},
|
||||
modifier = Modifier.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
@ -262,12 +346,14 @@ private fun LazyColumnContent(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ObjectTypeItem(
|
||||
modifier: Modifier,
|
||||
name: String,
|
||||
emoji: String,
|
||||
onItemClicked: () -> Unit
|
||||
onItemClicked: () -> Unit,
|
||||
onItemLongClicked: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
@ -278,7 +364,14 @@ fun ObjectTypeItem(
|
|||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable { onItemClicked() },
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onItemClicked()
|
||||
},
|
||||
onLongClick = {
|
||||
onItemLongClicked()
|
||||
}
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<resources>
|
||||
<string name="amplitude_api_key">1ba981d1a9afb8af8c81847ef3383a20</string>
|
||||
<string name="amplitude_api_key_debug">b9791dd64a1e9f07a330a4ac9feb1f10</string>
|
||||
<string name="create_object_section_pinned">Pinned</string>
|
||||
<string name="any_object_creation_menu_pin_on_top">Pin on top</string>
|
||||
<string name="any_object_creation_menu_unpin">Unpin</string>
|
||||
</resources>
|
||||
|
|
|
@ -6,12 +6,15 @@ import com.anytypeio.anytype.core_models.Wallpaper
|
|||
import com.anytypeio.anytype.core_models.WidgetSession
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserSettingsCache {
|
||||
suspend fun setCurrentSpace(space: SpaceId)
|
||||
suspend fun getCurrentSpace(): SpaceId?
|
||||
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
||||
suspend fun getDefaultObjectType(space: SpaceId): TypeId?
|
||||
suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>)
|
||||
fun getPinnedObjectTypes(space: SpaceId) : Flow<List<TypeId>>
|
||||
suspend fun setWallpaper(space: Id, wallpaper: Wallpaper)
|
||||
suspend fun getWallpaper(space: Id) : Wallpaper
|
||||
suspend fun setThemeMode(mode: ThemeMode)
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.WidgetSession
|
|||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSettingsRepository {
|
||||
|
||||
|
@ -28,6 +29,17 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
|
|||
space: SpaceId
|
||||
): TypeId? = cache.getDefaultObjectType(space = space)
|
||||
|
||||
override suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>) {
|
||||
cache.setPinnedObjectTypes(
|
||||
space = space,
|
||||
types = types
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPinnedObjectTypes(space: SpaceId): Flow<List<TypeId>> {
|
||||
return cache.getPinnedObjectTypes(space = space)
|
||||
}
|
||||
|
||||
override suspend fun setThemeMode(mode: ThemeMode) {
|
||||
cache.setThemeMode(mode)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,9 @@ abstract class FlowInteractor<in P, R>(
|
|||
private val context: CoroutineContext
|
||||
) {
|
||||
protected abstract fun build() : Flow<R>
|
||||
protected abstract fun build(params: P) : Flow<R>
|
||||
fun flow() : Flow<R> = build().flowOn(context)
|
||||
fun flow(params: P) : Flow<R> = build(params).flowOn(context)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_models.Wallpaper
|
|||
import com.anytypeio.anytype.core_models.WidgetSession
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserSettingsRepository {
|
||||
|
||||
|
@ -18,6 +19,9 @@ interface UserSettingsRepository {
|
|||
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
||||
suspend fun getDefaultObjectType(space: SpaceId): TypeId?
|
||||
|
||||
suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>)
|
||||
fun getPinnedObjectTypes(space: SpaceId) : Flow<List<TypeId>>
|
||||
|
||||
suspend fun setThemeMode(mode: ThemeMode)
|
||||
suspend fun getThemeMode(): ThemeMode
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.anytypeio.anytype.domain.types
|
||||
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.FlowInteractor
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
class GetPinnedObjectTypes @Inject constructor(
|
||||
private val repo: UserSettingsRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : FlowInteractor<GetPinnedObjectTypes.Params, List<TypeId>>(dispatchers.io) {
|
||||
|
||||
override fun build(): Flow<List<TypeId>> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun build(params: Params) = repo.getPinnedObjectTypes(
|
||||
space = params.space
|
||||
).catch { emit(emptyList()) }
|
||||
|
||||
class Params(val space: SpaceId)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.anytypeio.anytype.domain.types
|
||||
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
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 SetPinnedObjectTypes @Inject constructor(
|
||||
private val repo: UserSettingsRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<SetPinnedObjectTypes.Params, Unit>(dispatchers.io) {
|
||||
|
||||
override suspend fun doWork(params: Params) {
|
||||
repo.setPinnedObjectTypes(
|
||||
space = params.space,
|
||||
types = params.types
|
||||
)
|
||||
}
|
||||
|
||||
class Params(
|
||||
val space: SpaceId,
|
||||
val types: List<TypeId>
|
||||
)
|
||||
}
|
|
@ -28,4 +28,6 @@ class RestoreWallpaper(
|
|||
.catch {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun build(params: Unit): Flow<Unit> = build()
|
||||
}
|
|
@ -64,6 +64,7 @@ timberVersion = '5.0.1'
|
|||
protobufJavaVersion = '3.9.2'
|
||||
protocVersion = '3.9.0'
|
||||
roomVersion = '2.5.2'
|
||||
dataStoreVersion = '1.0.0'
|
||||
amplitudeVersion = '2.36.1'
|
||||
coilComposeVersion = '2.2.2'
|
||||
sentryVersion = '6.0.0'
|
||||
|
@ -153,6 +154,7 @@ protobufJava = { module = "com.google.protobuf:protobuf-java", version.ref = "pr
|
|||
protobufJavaUtil = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobufJavaVersion" }
|
||||
protoc = { module = "com.google.protobuf:protoc", version.ref = "protocVersion" }
|
||||
room = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
||||
dataStore = { module = "androidx.datastore:datastore", version.ref = "dataStoreVersion" }
|
||||
roomKtx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
|
||||
annotations = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
|
||||
roomTesting = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
|
||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
id "kotlin-android"
|
||||
id "kotlin-kapt"
|
||||
id "kotlinx-serialization"
|
||||
id "com.squareup.wire"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -18,6 +19,7 @@ dependencies {
|
|||
|
||||
implementation libs.room
|
||||
implementation libs.roomKtx
|
||||
implementation libs.dataStore
|
||||
|
||||
kapt libs.annotations
|
||||
|
||||
|
@ -30,6 +32,7 @@ dependencies {
|
|||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.robolectric
|
||||
testImplementation libs.archCoreTesting
|
||||
testImplementation libs.androidXTestCore
|
||||
testImplementation libs.coroutineTesting
|
||||
}
|
||||
|
||||
|
@ -42,4 +45,9 @@ android {
|
|||
jvmToolchain(17)
|
||||
}
|
||||
namespace 'com.anytypeio.anytype.persistence'
|
||||
}
|
||||
|
||||
wire {
|
||||
protoPath { srcDir 'src/main/proto' }
|
||||
kotlin {}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.anytypeio.anytype.persistence.preferences
|
||||
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.anytypeio.anytype.persistence.SpacePreferences
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object SpacePrefSerializer : Serializer<SpacePreferences> {
|
||||
override val defaultValue: SpacePreferences = SpacePreferences()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): SpacePreferences {
|
||||
return SpacePreferences.ADAPTER.decode(input)
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: SpacePreferences, output: OutputStream) {
|
||||
SpacePreferences.ADAPTER.encode(
|
||||
stream = output,
|
||||
value = t
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const val SPACE_PREFERENCE_FILENAME = "space-preferences.pb"
|
|
@ -1,6 +1,9 @@
|
|||
package com.anytypeio.anytype.persistence.repo
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.dataStore
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.NO_VALUE
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
|
@ -9,6 +12,8 @@ import com.anytypeio.anytype.core_models.WidgetSession
|
|||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.data.auth.repo.UserSettingsCache
|
||||
import com.anytypeio.anytype.persistence.SpacePreference
|
||||
import com.anytypeio.anytype.persistence.SpacePreferences
|
||||
import com.anytypeio.anytype.persistence.common.JsonString
|
||||
import com.anytypeio.anytype.persistence.common.deserializeWallpaperSettings
|
||||
import com.anytypeio.anytype.persistence.common.serializeWallpaperSettings
|
||||
|
@ -16,8 +21,20 @@ import com.anytypeio.anytype.persistence.common.toJsonString
|
|||
import com.anytypeio.anytype.persistence.common.toStringMap
|
||||
import com.anytypeio.anytype.persistence.model.asSettings
|
||||
import com.anytypeio.anytype.persistence.model.asWallpaper
|
||||
import com.anytypeio.anytype.persistence.preferences.SPACE_PREFERENCE_FILENAME
|
||||
import com.anytypeio.anytype.persistence.preferences.SpacePrefSerializer
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSettingsCache {
|
||||
class DefaultUserSettingsCache(
|
||||
private val prefs: SharedPreferences,
|
||||
private val context: Context
|
||||
) : UserSettingsCache {
|
||||
|
||||
private val Context.spacePrefsStore: DataStore<SpacePreferences> by dataStore(
|
||||
fileName = SPACE_PREFERENCE_FILENAME,
|
||||
serializer = SpacePrefSerializer
|
||||
)
|
||||
|
||||
override suspend fun setCurrentSpace(space: SpaceId) {
|
||||
prefs.edit()
|
||||
|
@ -46,6 +63,22 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
|||
.edit()
|
||||
.putString(DEFAULT_OBJECT_TYPES_KEY, updated.toJsonString())
|
||||
.apply()
|
||||
|
||||
context
|
||||
.spacePrefsStore
|
||||
.updateData { prefs ->
|
||||
val givenSpacePreferences = prefs.preferences.getOrDefault(
|
||||
space.id,
|
||||
SpacePreference()
|
||||
)
|
||||
val updatedSpacePreferences = givenSpacePreferences.copy(
|
||||
defaultObjectTypeKey = type.id
|
||||
)
|
||||
|
||||
val result = prefs.preferences + mapOf(space.id to updatedSpacePreferences)
|
||||
|
||||
prefs.copy(preferences = result)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDefaultObjectType(space: SpaceId): TypeId? {
|
||||
|
@ -168,7 +201,41 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
|||
.apply()
|
||||
}
|
||||
|
||||
override suspend fun setPinnedObjectTypes(space: SpaceId, types: List<TypeId>) {
|
||||
context.spacePrefsStore.updateData { existingPreferences ->
|
||||
val givenSpacePreference = existingPreferences
|
||||
.preferences
|
||||
.getOrDefault(key = space.id, defaultValue = SpacePreference())
|
||||
|
||||
val updated = givenSpacePreference.copy(
|
||||
pinnedObjectTypeKeys = types.map { type -> type.id }
|
||||
)
|
||||
|
||||
val result = buildMap {
|
||||
putAll(existingPreferences.preferences)
|
||||
put(key = space.id, updated)
|
||||
}
|
||||
|
||||
SpacePreferences(
|
||||
preferences = result
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPinnedObjectTypes(space: SpaceId): Flow<List<TypeId>> {
|
||||
return context.spacePrefsStore
|
||||
.data
|
||||
.map { preferences ->
|
||||
preferences
|
||||
.preferences[space.id]
|
||||
?.pinnedObjectTypeKeys?.map { id -> TypeId(id) } ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
|
||||
// Clearing shared preferences
|
||||
|
||||
prefs.edit()
|
||||
.remove(DEFAULT_OBJECT_TYPE_ID_KEY)
|
||||
.remove(DEFAULT_OBJECT_TYPE_NAME_KEY)
|
||||
|
@ -176,6 +243,12 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
|||
.remove(ACTIVE_WIDGETS_VIEWS_KEY)
|
||||
.remove(CURRENT_SPACE_KEY)
|
||||
.apply()
|
||||
|
||||
// Clearing data stores
|
||||
|
||||
context.spacePrefsStore.updateData {
|
||||
SpacePreferences(emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.anytypeio.anytype.persistence";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message SpacePreferences {
|
||||
// maps space id to space preference
|
||||
map <string, SpacePreference> preferences = 1;
|
||||
}
|
||||
|
||||
message SpacePreference {
|
||||
optional string defaultObjectTypeKey = 1;
|
||||
repeated string pinnedObjectTypeKeys = 2;
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.anytypeio.anytype.persistence
|
|||
|
||||
import android.content.Context
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
|
@ -32,7 +33,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save and return default wallpaper`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
@ -55,7 +57,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save and return gradient wallpaper`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
@ -81,7 +84,8 @@ class UserSettingsCacheTest {
|
|||
fun `should not save wallpaper if space id is empty, should return default`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = ""
|
||||
|
@ -108,7 +112,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save and return solid-color wallpaper`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
@ -134,7 +139,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save and return image wallpaper`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
@ -160,7 +166,8 @@ class UserSettingsCacheTest {
|
|||
fun `should return default wallpaper`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
@ -178,7 +185,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save new default object type for given space`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space = SpaceId(MockDataFactory.randomUuid())
|
||||
|
@ -210,7 +218,8 @@ class UserSettingsCacheTest {
|
|||
fun `should save default object type for two given spaces`() = runTest {
|
||||
|
||||
val cache = DefaultUserSettingsCache(
|
||||
prefs = defaultPrefs
|
||||
prefs = defaultPrefs,
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
)
|
||||
|
||||
val space1 = SpaceId(MockDataFactory.randomUuid())
|
||||
|
|
|
@ -11,17 +11,23 @@ import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
|
|||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.ext.mapToObjectWrapperType
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeKey
|
||||
import com.anytypeio.anytype.domain.base.Resultat
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
|
||||
import com.anytypeio.anytype.domain.spaces.AddObjectToSpace
|
||||
import com.anytypeio.anytype.domain.types.GetPinnedObjectTypes
|
||||
import com.anytypeio.anytype.domain.types.SetPinnedObjectTypes
|
||||
import com.anytypeio.anytype.domain.workspace.SpaceManager
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -34,6 +40,8 @@ class SelectObjectTypeViewModel(
|
|||
private val getObjectTypes: GetObjectTypes,
|
||||
private val spaceManager: SpaceManager,
|
||||
private val addObjectToSpace: AddObjectToSpace,
|
||||
private val setPinnedObjectTypes: SetPinnedObjectTypes,
|
||||
private val getPinnedObjectTypes: GetPinnedObjectTypes
|
||||
) : BaseViewModel() {
|
||||
|
||||
val viewState = MutableStateFlow<SelectTypeViewState>(SelectTypeViewState.Loading)
|
||||
|
@ -49,7 +57,7 @@ class SelectObjectTypeViewModel(
|
|||
viewModelScope.launch {
|
||||
space = spaceManager.get()
|
||||
query.onStart { emit(EMPTY_QUERY) }.flatMapLatest { query ->
|
||||
getObjectTypes.stream(
|
||||
val types = getObjectTypes.stream(
|
||||
GetObjectTypes.Params(
|
||||
sorts = ObjectSearchConstants.defaultObjectTypeSearchSorts(),
|
||||
filters = ObjectSearchConstants.filterTypes(
|
||||
|
@ -65,10 +73,23 @@ class SelectObjectTypeViewModel(
|
|||
keys = ObjectSearchConstants.defaultKeysObjectType,
|
||||
query = query
|
||||
)
|
||||
).filterIsInstance<Resultat.Success<List<ObjectWrapper.Type>>>().map { result ->
|
||||
).filterIsInstance<Resultat.Success<List<ObjectWrapper.Type>>>()
|
||||
|
||||
combine(
|
||||
types,
|
||||
getPinnedObjectTypes.flow(GetPinnedObjectTypes.Params(SpaceId(space)))
|
||||
) { result, pinned ->
|
||||
_objectTypes.clear()
|
||||
_objectTypes.addAll(result.getOrNull() ?: emptyList())
|
||||
|
||||
val pinnedObjectTypesIds = pinned.map { it.id }
|
||||
|
||||
val allTypes = (result.getOrNull() ?: emptyList())
|
||||
|
||||
val pinnedTypes = allTypes
|
||||
.filter { pinnedObjectTypesIds.contains(it.id) }
|
||||
.sortedBy { obj -> pinnedObjectTypesIds.indexOf(obj.id) }
|
||||
|
||||
val (allUserTypes, allLibraryTypes) = allTypes.partition { type ->
|
||||
type.getValue<Id>(Relations.SPACE_ID) == space
|
||||
}
|
||||
|
@ -78,33 +99,57 @@ class SelectObjectTypeViewModel(
|
|||
val (groups, objects) = allUserTypes.partition { type ->
|
||||
type.uniqueKey == ObjectTypeUniqueKeys.SET || type.uniqueKey == ObjectTypeUniqueKeys.COLLECTION
|
||||
}
|
||||
val notPinnedObjects = objects.filter { !pinnedObjectTypesIds.contains(it.id) }
|
||||
buildList {
|
||||
if (pinnedTypes.isNotEmpty()) {
|
||||
add(
|
||||
SelectTypeView.Section.Pinned
|
||||
)
|
||||
addAll(
|
||||
pinnedTypes.mapIndexed { index, type ->
|
||||
SelectTypeView.Type(
|
||||
id = type.id,
|
||||
typeKey = type.uniqueKey,
|
||||
name = type.name.orEmpty(),
|
||||
icon = type.iconEmoji.orEmpty(),
|
||||
isPinned = true,
|
||||
isFirstInSection = index == 0
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (groups.isNotEmpty()) {
|
||||
add(
|
||||
SelectTypeView.Section.Groups
|
||||
)
|
||||
addAll(
|
||||
groups.map { type ->
|
||||
groups.mapIndexed { index, type ->
|
||||
SelectTypeView.Type(
|
||||
id = type.id,
|
||||
typeKey = type.uniqueKey,
|
||||
name = type.name.orEmpty(),
|
||||
icon = type.iconEmoji.orEmpty()
|
||||
icon = type.iconEmoji.orEmpty(),
|
||||
isFirstInSection = index == 0,
|
||||
isPinnable = false,
|
||||
isPinned = false,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (objects.isNotEmpty()) {
|
||||
if (notPinnedObjects.isNotEmpty()) {
|
||||
add(
|
||||
SelectTypeView.Section.Objects
|
||||
)
|
||||
addAll(
|
||||
objects.map { type ->
|
||||
notPinnedObjects.mapIndexed { index, type ->
|
||||
SelectTypeView.Type(
|
||||
id = type.id,
|
||||
typeKey = type.uniqueKey,
|
||||
name = type.name.orEmpty(),
|
||||
icon = type.iconEmoji.orEmpty()
|
||||
icon = type.iconEmoji.orEmpty(),
|
||||
isPinnable = true,
|
||||
isFirstInSection = index == 0,
|
||||
isPinned = false
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -112,13 +157,16 @@ class SelectObjectTypeViewModel(
|
|||
if (filteredLibraryTypes.isNotEmpty()) {
|
||||
add(SelectTypeView.Section.Library)
|
||||
addAll(
|
||||
filteredLibraryTypes.map { type ->
|
||||
filteredLibraryTypes.mapIndexed { index, type ->
|
||||
SelectTypeView.Type(
|
||||
id = type.id,
|
||||
typeKey = type.uniqueKey,
|
||||
name = type.name.orEmpty(),
|
||||
icon = type.iconEmoji.orEmpty(),
|
||||
isFromLibrary = true
|
||||
isFromLibrary = true,
|
||||
isPinned = false,
|
||||
isPinnable = false,
|
||||
isFirstInSection = index == 0
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -142,6 +190,49 @@ class SelectObjectTypeViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onPinTypeClicked(typeView: SelectTypeView.Type) {
|
||||
Timber.d("onPinTypeClicked: ${typeView.id}")
|
||||
val state = viewState.value
|
||||
if (state is SelectTypeViewState.Content) {
|
||||
val pinned = buildSet {
|
||||
add(TypeId(typeView.id))
|
||||
state.views.forEach { view ->
|
||||
if (view is SelectTypeView.Type && view.isPinned)
|
||||
add(TypeId(view.id))
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
setPinnedObjectTypes.async(
|
||||
SetPinnedObjectTypes.Params(
|
||||
space = SpaceId(id = space),
|
||||
types = pinned.toList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onUnpinTypeClicked(typeView: SelectTypeView.Type) {
|
||||
Timber.d("onUnpinTypeClicked: ${typeView.id}")
|
||||
val state = viewState.value
|
||||
if (state is SelectTypeViewState.Content) {
|
||||
val pinned = buildSet {
|
||||
state.views.forEach { view ->
|
||||
if (view is SelectTypeView.Type && view.isPinned && view.id != typeView.id)
|
||||
add(TypeId(view.id))
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
setPinnedObjectTypes.async(
|
||||
SetPinnedObjectTypes.Params(
|
||||
space = SpaceId(id = space),
|
||||
types = pinned.toList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onTypeClicked(typeView: SelectTypeView.Type) {
|
||||
viewModelScope.launch {
|
||||
if (typeView.isFromLibrary) {
|
||||
|
@ -179,7 +270,9 @@ class SelectObjectTypeViewModel(
|
|||
private val params: Params,
|
||||
private val getObjectTypes: GetObjectTypes,
|
||||
private val spaceManager: SpaceManager,
|
||||
private val addObjectToSpace: AddObjectToSpace
|
||||
private val addObjectToSpace: AddObjectToSpace,
|
||||
private val setPinnedObjectTypes: SetPinnedObjectTypes,
|
||||
private val getPinnedObjectTypes: GetPinnedObjectTypes
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(
|
||||
|
@ -188,7 +281,9 @@ class SelectObjectTypeViewModel(
|
|||
params = params,
|
||||
getObjectTypes = getObjectTypes,
|
||||
spaceManager = spaceManager,
|
||||
addObjectToSpace = addObjectToSpace
|
||||
addObjectToSpace = addObjectToSpace,
|
||||
setPinnedObjectTypes = setPinnedObjectTypes,
|
||||
getPinnedObjectTypes = getPinnedObjectTypes
|
||||
) as T
|
||||
}
|
||||
|
||||
|
@ -205,6 +300,7 @@ sealed class SelectTypeViewState{
|
|||
|
||||
sealed class SelectTypeView {
|
||||
sealed class Section : SelectTypeView() {
|
||||
object Pinned : Section()
|
||||
object Objects : Section()
|
||||
object Groups : Section()
|
||||
object Library : Section()
|
||||
|
@ -215,7 +311,10 @@ sealed class SelectTypeView {
|
|||
val typeKey: Key,
|
||||
val name: String,
|
||||
val icon: String,
|
||||
val isFromLibrary: Boolean = false
|
||||
val isFromLibrary: Boolean = false,
|
||||
val isPinned: Boolean = false,
|
||||
val isFirstInSection: Boolean = false,
|
||||
val isPinnable: Boolean = true
|
||||
) : SelectTypeView()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue