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.di.common.ComponentDependencies
|
||||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
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.domain.workspace.SpaceManager
|
||||||
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
|
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
|
||||||
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
|
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
|
||||||
|
@ -48,4 +49,5 @@ interface SelectObjectTypeDependencies : ComponentDependencies {
|
||||||
fun analytics(): Analytics
|
fun analytics(): Analytics
|
||||||
fun dispatchers(): AppCoroutineDispatchers
|
fun dispatchers(): AppCoroutineDispatchers
|
||||||
fun spaceManager(): SpaceManager
|
fun spaceManager(): SpaceManager
|
||||||
|
fun userSettingsRepo(): UserSettingsRepository
|
||||||
}
|
}
|
|
@ -277,7 +277,11 @@ object DataModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUserSettingsCache(
|
fun provideUserSettingsCache(
|
||||||
@Named("default") prefs: SharedPreferences,
|
@Named("default") prefs: SharedPreferences,
|
||||||
): UserSettingsCache = DefaultUserSettingsCache(prefs)
|
context: Context
|
||||||
|
): UserSettingsCache = DefaultUserSettingsCache(
|
||||||
|
prefs = prefs,
|
||||||
|
context = context
|
||||||
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.anytypeio.anytype.R
|
import com.anytypeio.anytype.R
|
||||||
import com.anytypeio.anytype.core_models.Key
|
import com.anytypeio.anytype.core_models.Key
|
||||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
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.presentation.objects.SelectObjectTypeViewModel
|
||||||
import com.anytypeio.anytype.ui.settings.typography
|
import com.anytypeio.anytype.ui.settings.typography
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
||||||
|
|
||||||
|
@ -48,6 +51,20 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
||||||
SelectObjectTypeScreen(
|
SelectObjectTypeScreen(
|
||||||
state = vm.viewState.collectAsStateWithLifecycle().value,
|
state = vm.viewState.collectAsStateWithLifecycle().value,
|
||||||
onTypeClicked = vm::onTypeClicked,
|
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,
|
onQueryChanged = vm::onQueryChanged,
|
||||||
onFocused = {
|
onFocused = {
|
||||||
skipCollapsed()
|
skipCollapsed()
|
||||||
|
@ -89,6 +106,7 @@ class SelectObjectTypeFragment : BaseBottomSheetComposeFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXCLUDED_TYPE_KEYS_ARG_KEY = "arg.create-object-of-type.excluded-type-keys"
|
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(
|
fun newInstance(
|
||||||
excludedTypeKeys: List<Key>,
|
excludedTypeKeys: List<Key>,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
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.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextFieldDefaults
|
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.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.anytypeio.anytype.R
|
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.emojifier.Emojifier
|
||||||
import com.anytypeio.anytype.presentation.objects.SelectTypeView
|
import com.anytypeio.anytype.presentation.objects.SelectTypeView
|
||||||
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
|
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -78,13 +82,17 @@ fun PreviewScreen() {
|
||||||
onTypeClicked = {},
|
onTypeClicked = {},
|
||||||
state = SelectTypeViewState.Loading,
|
state = SelectTypeViewState.Loading,
|
||||||
onQueryChanged = {},
|
onQueryChanged = {},
|
||||||
onFocused = {}
|
onFocused = {},
|
||||||
|
onUnpinTypeClicked = {},
|
||||||
|
onPinOnTopClicked = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectObjectTypeScreen(
|
fun SelectObjectTypeScreen(
|
||||||
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onPinOnTopClicked: (SelectTypeView.Type) -> Unit,
|
||||||
onQueryChanged: (String) -> Unit,
|
onQueryChanged: (String) -> Unit,
|
||||||
onFocused: () -> Unit,
|
onFocused: () -> Unit,
|
||||||
state: SelectTypeViewState
|
state: SelectTypeViewState
|
||||||
|
@ -104,18 +112,30 @@ fun SelectObjectTypeScreen(
|
||||||
onFocused = onFocused
|
onFocused = onFocused
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
ScreenContent(state, onTypeClicked)
|
ScreenContent(
|
||||||
|
state = state,
|
||||||
|
onTypeClicked = onTypeClicked,
|
||||||
|
onPinOnTopClicked = onPinOnTopClicked,
|
||||||
|
onUnpinTypeClicked = onUnpinTypeClicked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenContent(
|
private fun ScreenContent(
|
||||||
state: SelectTypeViewState,
|
state: SelectTypeViewState,
|
||||||
onTypeClicked: (SelectTypeView.Type) -> Unit
|
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is SelectTypeViewState.Content -> {
|
is SelectTypeViewState.Content -> {
|
||||||
FlowRowContent(state.views, onTypeClicked)
|
FlowRowContent(
|
||||||
|
views = state.views,
|
||||||
|
onTypeClicked = onTypeClicked,
|
||||||
|
onPinOnTopClicked = onPinOnTopClicked,
|
||||||
|
onUnpinTypeClicked = onUnpinTypeClicked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SelectTypeViewState.Empty -> {
|
SelectTypeViewState.Empty -> {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
@ -149,7 +169,9 @@ private fun ScreenContent(
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
private fun FlowRowContent(
|
private fun FlowRowContent(
|
||||||
views: List<SelectTypeView>,
|
views: List<SelectTypeView>,
|
||||||
onTypeClicked: (SelectTypeView.Type) -> Unit
|
onTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
|
||||||
|
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -162,13 +184,62 @@ private fun FlowRowContent(
|
||||||
views.forEach { view ->
|
views.forEach { view ->
|
||||||
when (view) {
|
when (view) {
|
||||||
is SelectTypeView.Type -> {
|
is SelectTypeView.Type -> {
|
||||||
ObjectTypeItem(
|
val isMenuExpanded = remember {
|
||||||
name = view.name,
|
mutableStateOf(false)
|
||||||
emoji = view.icon,
|
}
|
||||||
onItemClicked = throttledClick(
|
Box {
|
||||||
onClick = { onTypeClicked(view) }
|
ObjectTypeItem(
|
||||||
),
|
name = view.name,
|
||||||
modifier = Modifier
|
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 -> {
|
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 -> {
|
is SelectTypeView.Section.Library -> {
|
||||||
item(
|
item(
|
||||||
key = view.javaClass.name,
|
key = view.javaClass.name,
|
||||||
|
@ -252,6 +333,9 @@ private fun LazyColumnContent(
|
||||||
onTypeClicked(view)
|
onTypeClicked(view)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
onItemLongClicked = {
|
||||||
|
|
||||||
|
},
|
||||||
modifier = Modifier.animateItemPlacement()
|
modifier = Modifier.animateItemPlacement()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -262,12 +346,14 @@ private fun LazyColumnContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ObjectTypeItem(
|
fun ObjectTypeItem(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
name: String,
|
name: String,
|
||||||
emoji: String,
|
emoji: String,
|
||||||
onItemClicked: () -> Unit
|
onItemClicked: () -> Unit,
|
||||||
|
onItemLongClicked: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
@ -278,7 +364,14 @@ fun ObjectTypeItem(
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.clickable { onItemClicked() },
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
onItemClicked()
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onItemLongClicked()
|
||||||
|
}
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Spacer(
|
Spacer(
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="amplitude_api_key">1ba981d1a9afb8af8c81847ef3383a20</string>
|
<string name="amplitude_api_key">1ba981d1a9afb8af8c81847ef3383a20</string>
|
||||||
<string name="amplitude_api_key_debug">b9791dd64a1e9f07a330a4ac9feb1f10</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>
|
</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.WidgetSession
|
||||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface UserSettingsCache {
|
interface UserSettingsCache {
|
||||||
suspend fun setCurrentSpace(space: SpaceId)
|
suspend fun setCurrentSpace(space: SpaceId)
|
||||||
suspend fun getCurrentSpace(): SpaceId?
|
suspend fun getCurrentSpace(): SpaceId?
|
||||||
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
||||||
suspend fun getDefaultObjectType(space: SpaceId): 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 setWallpaper(space: Id, wallpaper: Wallpaper)
|
||||||
suspend fun getWallpaper(space: Id) : Wallpaper
|
suspend fun getWallpaper(space: Id) : Wallpaper
|
||||||
suspend fun setThemeMode(mode: ThemeMode)
|
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.SpaceId
|
||||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSettingsRepository {
|
class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSettingsRepository {
|
||||||
|
|
||||||
|
@ -28,6 +29,17 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
|
||||||
space: SpaceId
|
space: SpaceId
|
||||||
): TypeId? = cache.getDefaultObjectType(space = space)
|
): 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) {
|
override suspend fun setThemeMode(mode: ThemeMode) {
|
||||||
cache.setThemeMode(mode)
|
cache.setThemeMode(mode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,9 @@ abstract class FlowInteractor<in P, R>(
|
||||||
private val context: CoroutineContext
|
private val context: CoroutineContext
|
||||||
) {
|
) {
|
||||||
protected abstract fun build() : Flow<R>
|
protected abstract fun build() : Flow<R>
|
||||||
|
protected abstract fun build(params: P) : Flow<R>
|
||||||
fun flow() : Flow<R> = build().flowOn(context)
|
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.WidgetSession
|
||||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface UserSettingsRepository {
|
interface UserSettingsRepository {
|
||||||
|
|
||||||
|
@ -18,6 +19,9 @@ interface UserSettingsRepository {
|
||||||
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
suspend fun setDefaultObjectType(space: SpaceId, type: TypeId)
|
||||||
suspend fun getDefaultObjectType(space: SpaceId): 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 setThemeMode(mode: ThemeMode)
|
||||||
suspend fun getThemeMode(): 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 {
|
.catch {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun build(params: Unit): Flow<Unit> = build()
|
||||||
}
|
}
|
|
@ -64,6 +64,7 @@ timberVersion = '5.0.1'
|
||||||
protobufJavaVersion = '3.9.2'
|
protobufJavaVersion = '3.9.2'
|
||||||
protocVersion = '3.9.0'
|
protocVersion = '3.9.0'
|
||||||
roomVersion = '2.5.2'
|
roomVersion = '2.5.2'
|
||||||
|
dataStoreVersion = '1.0.0'
|
||||||
amplitudeVersion = '2.36.1'
|
amplitudeVersion = '2.36.1'
|
||||||
coilComposeVersion = '2.2.2'
|
coilComposeVersion = '2.2.2'
|
||||||
sentryVersion = '6.0.0'
|
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" }
|
protobufJavaUtil = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobufJavaVersion" }
|
||||||
protoc = { module = "com.google.protobuf:protoc", version.ref = "protocVersion" }
|
protoc = { module = "com.google.protobuf:protoc", version.ref = "protocVersion" }
|
||||||
room = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
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" }
|
roomKtx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
|
||||||
annotations = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
|
annotations = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
|
||||||
roomTesting = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
|
roomTesting = { module = "androidx.room:room-testing", version.ref = "roomVersion" }
|
||||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "kotlin-kapt"
|
id "kotlin-kapt"
|
||||||
id "kotlinx-serialization"
|
id "kotlinx-serialization"
|
||||||
|
id "com.squareup.wire"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -18,6 +19,7 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.room
|
implementation libs.room
|
||||||
implementation libs.roomKtx
|
implementation libs.roomKtx
|
||||||
|
implementation libs.dataStore
|
||||||
|
|
||||||
kapt libs.annotations
|
kapt libs.annotations
|
||||||
|
|
||||||
|
@ -30,6 +32,7 @@ dependencies {
|
||||||
testImplementation libs.mockitoKotlin
|
testImplementation libs.mockitoKotlin
|
||||||
testImplementation libs.robolectric
|
testImplementation libs.robolectric
|
||||||
testImplementation libs.archCoreTesting
|
testImplementation libs.archCoreTesting
|
||||||
|
testImplementation libs.androidXTestCore
|
||||||
testImplementation libs.coroutineTesting
|
testImplementation libs.coroutineTesting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,4 +45,9 @@ android {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
}
|
}
|
||||||
namespace 'com.anytypeio.anytype.persistence'
|
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
|
package com.anytypeio.anytype.persistence.repo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
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.Id
|
||||||
import com.anytypeio.anytype.core_models.NO_VALUE
|
import com.anytypeio.anytype.core_models.NO_VALUE
|
||||||
import com.anytypeio.anytype.core_models.ThemeMode
|
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.SpaceId
|
||||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||||
import com.anytypeio.anytype.data.auth.repo.UserSettingsCache
|
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.JsonString
|
||||||
import com.anytypeio.anytype.persistence.common.deserializeWallpaperSettings
|
import com.anytypeio.anytype.persistence.common.deserializeWallpaperSettings
|
||||||
import com.anytypeio.anytype.persistence.common.serializeWallpaperSettings
|
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.common.toStringMap
|
||||||
import com.anytypeio.anytype.persistence.model.asSettings
|
import com.anytypeio.anytype.persistence.model.asSettings
|
||||||
import com.anytypeio.anytype.persistence.model.asWallpaper
|
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) {
|
override suspend fun setCurrentSpace(space: SpaceId) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
|
@ -46,6 +63,22 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
||||||
.edit()
|
.edit()
|
||||||
.putString(DEFAULT_OBJECT_TYPES_KEY, updated.toJsonString())
|
.putString(DEFAULT_OBJECT_TYPES_KEY, updated.toJsonString())
|
||||||
.apply()
|
.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? {
|
override suspend fun getDefaultObjectType(space: SpaceId): TypeId? {
|
||||||
|
@ -168,7 +201,41 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
||||||
.apply()
|
.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() {
|
override suspend fun clear() {
|
||||||
|
|
||||||
|
// Clearing shared preferences
|
||||||
|
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove(DEFAULT_OBJECT_TYPE_ID_KEY)
|
.remove(DEFAULT_OBJECT_TYPE_ID_KEY)
|
||||||
.remove(DEFAULT_OBJECT_TYPE_NAME_KEY)
|
.remove(DEFAULT_OBJECT_TYPE_NAME_KEY)
|
||||||
|
@ -176,6 +243,12 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
||||||
.remove(ACTIVE_WIDGETS_VIEWS_KEY)
|
.remove(ACTIVE_WIDGETS_VIEWS_KEY)
|
||||||
.remove(CURRENT_SPACE_KEY)
|
.remove(CURRENT_SPACE_KEY)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
|
// Clearing data stores
|
||||||
|
|
||||||
|
context.spacePrefsStore.updateData {
|
||||||
|
SpacePreferences(emptyMap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
14
persistence/src/main/proto/preferences.proto
Normal file
14
persistence/src/main/proto/preferences.proto
Normal file
|
@ -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 android.content.Context
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
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.Wallpaper
|
||||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||||
|
@ -32,7 +33,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save and return default wallpaper`() = runTest {
|
fun `should save and return default wallpaper`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = MockDataFactory.randomUuid()
|
val space = MockDataFactory.randomUuid()
|
||||||
|
@ -55,7 +57,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save and return gradient wallpaper`() = runTest {
|
fun `should save and return gradient wallpaper`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = MockDataFactory.randomUuid()
|
val space = MockDataFactory.randomUuid()
|
||||||
|
@ -81,7 +84,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should not save wallpaper if space id is empty, should return default`() = runTest {
|
fun `should not save wallpaper if space id is empty, should return default`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = ""
|
val space = ""
|
||||||
|
@ -108,7 +112,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save and return solid-color wallpaper`() = runTest {
|
fun `should save and return solid-color wallpaper`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = MockDataFactory.randomUuid()
|
val space = MockDataFactory.randomUuid()
|
||||||
|
@ -134,7 +139,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save and return image wallpaper`() = runTest {
|
fun `should save and return image wallpaper`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = MockDataFactory.randomUuid()
|
val space = MockDataFactory.randomUuid()
|
||||||
|
@ -160,7 +166,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should return default wallpaper`() = runTest {
|
fun `should return default wallpaper`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = MockDataFactory.randomUuid()
|
val space = MockDataFactory.randomUuid()
|
||||||
|
@ -178,7 +185,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save new default object type for given space`() = runTest {
|
fun `should save new default object type for given space`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space = SpaceId(MockDataFactory.randomUuid())
|
val space = SpaceId(MockDataFactory.randomUuid())
|
||||||
|
@ -210,7 +218,8 @@ class UserSettingsCacheTest {
|
||||||
fun `should save default object type for two given spaces`() = runTest {
|
fun `should save default object type for two given spaces`() = runTest {
|
||||||
|
|
||||||
val cache = DefaultUserSettingsCache(
|
val cache = DefaultUserSettingsCache(
|
||||||
prefs = defaultPrefs
|
prefs = defaultPrefs,
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
)
|
)
|
||||||
|
|
||||||
val space1 = SpaceId(MockDataFactory.randomUuid())
|
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.ObjectWrapper
|
||||||
import com.anytypeio.anytype.core_models.Relations
|
import com.anytypeio.anytype.core_models.Relations
|
||||||
import com.anytypeio.anytype.core_models.ext.mapToObjectWrapperType
|
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.core_models.primitives.TypeKey
|
||||||
import com.anytypeio.anytype.domain.base.Resultat
|
import com.anytypeio.anytype.domain.base.Resultat
|
||||||
import com.anytypeio.anytype.domain.base.fold
|
import com.anytypeio.anytype.domain.base.fold
|
||||||
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
|
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
|
||||||
import com.anytypeio.anytype.domain.spaces.AddObjectToSpace
|
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.domain.workspace.SpaceManager
|
||||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -34,6 +40,8 @@ class SelectObjectTypeViewModel(
|
||||||
private val getObjectTypes: GetObjectTypes,
|
private val getObjectTypes: GetObjectTypes,
|
||||||
private val spaceManager: SpaceManager,
|
private val spaceManager: SpaceManager,
|
||||||
private val addObjectToSpace: AddObjectToSpace,
|
private val addObjectToSpace: AddObjectToSpace,
|
||||||
|
private val setPinnedObjectTypes: SetPinnedObjectTypes,
|
||||||
|
private val getPinnedObjectTypes: GetPinnedObjectTypes
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val viewState = MutableStateFlow<SelectTypeViewState>(SelectTypeViewState.Loading)
|
val viewState = MutableStateFlow<SelectTypeViewState>(SelectTypeViewState.Loading)
|
||||||
|
@ -49,7 +57,7 @@ class SelectObjectTypeViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
space = spaceManager.get()
|
space = spaceManager.get()
|
||||||
query.onStart { emit(EMPTY_QUERY) }.flatMapLatest { query ->
|
query.onStart { emit(EMPTY_QUERY) }.flatMapLatest { query ->
|
||||||
getObjectTypes.stream(
|
val types = getObjectTypes.stream(
|
||||||
GetObjectTypes.Params(
|
GetObjectTypes.Params(
|
||||||
sorts = ObjectSearchConstants.defaultObjectTypeSearchSorts(),
|
sorts = ObjectSearchConstants.defaultObjectTypeSearchSorts(),
|
||||||
filters = ObjectSearchConstants.filterTypes(
|
filters = ObjectSearchConstants.filterTypes(
|
||||||
|
@ -65,10 +73,23 @@ class SelectObjectTypeViewModel(
|
||||||
keys = ObjectSearchConstants.defaultKeysObjectType,
|
keys = ObjectSearchConstants.defaultKeysObjectType,
|
||||||
query = query
|
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.clear()
|
||||||
_objectTypes.addAll(result.getOrNull() ?: emptyList())
|
_objectTypes.addAll(result.getOrNull() ?: emptyList())
|
||||||
|
|
||||||
|
val pinnedObjectTypesIds = pinned.map { it.id }
|
||||||
|
|
||||||
val allTypes = (result.getOrNull() ?: emptyList())
|
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 ->
|
val (allUserTypes, allLibraryTypes) = allTypes.partition { type ->
|
||||||
type.getValue<Id>(Relations.SPACE_ID) == space
|
type.getValue<Id>(Relations.SPACE_ID) == space
|
||||||
}
|
}
|
||||||
|
@ -78,33 +99,57 @@ class SelectObjectTypeViewModel(
|
||||||
val (groups, objects) = allUserTypes.partition { type ->
|
val (groups, objects) = allUserTypes.partition { type ->
|
||||||
type.uniqueKey == ObjectTypeUniqueKeys.SET || type.uniqueKey == ObjectTypeUniqueKeys.COLLECTION
|
type.uniqueKey == ObjectTypeUniqueKeys.SET || type.uniqueKey == ObjectTypeUniqueKeys.COLLECTION
|
||||||
}
|
}
|
||||||
|
val notPinnedObjects = objects.filter { !pinnedObjectTypesIds.contains(it.id) }
|
||||||
buildList {
|
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()) {
|
if (groups.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
SelectTypeView.Section.Groups
|
SelectTypeView.Section.Groups
|
||||||
)
|
)
|
||||||
addAll(
|
addAll(
|
||||||
groups.map { type ->
|
groups.mapIndexed { index, type ->
|
||||||
SelectTypeView.Type(
|
SelectTypeView.Type(
|
||||||
id = type.id,
|
id = type.id,
|
||||||
typeKey = type.uniqueKey,
|
typeKey = type.uniqueKey,
|
||||||
name = type.name.orEmpty(),
|
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(
|
add(
|
||||||
SelectTypeView.Section.Objects
|
SelectTypeView.Section.Objects
|
||||||
)
|
)
|
||||||
addAll(
|
addAll(
|
||||||
objects.map { type ->
|
notPinnedObjects.mapIndexed { index, type ->
|
||||||
SelectTypeView.Type(
|
SelectTypeView.Type(
|
||||||
id = type.id,
|
id = type.id,
|
||||||
typeKey = type.uniqueKey,
|
typeKey = type.uniqueKey,
|
||||||
name = type.name.orEmpty(),
|
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()) {
|
if (filteredLibraryTypes.isNotEmpty()) {
|
||||||
add(SelectTypeView.Section.Library)
|
add(SelectTypeView.Section.Library)
|
||||||
addAll(
|
addAll(
|
||||||
filteredLibraryTypes.map { type ->
|
filteredLibraryTypes.mapIndexed { index, type ->
|
||||||
SelectTypeView.Type(
|
SelectTypeView.Type(
|
||||||
id = type.id,
|
id = type.id,
|
||||||
typeKey = type.uniqueKey,
|
typeKey = type.uniqueKey,
|
||||||
name = type.name.orEmpty(),
|
name = type.name.orEmpty(),
|
||||||
icon = type.iconEmoji.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) {
|
fun onTypeClicked(typeView: SelectTypeView.Type) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (typeView.isFromLibrary) {
|
if (typeView.isFromLibrary) {
|
||||||
|
@ -179,7 +270,9 @@ class SelectObjectTypeViewModel(
|
||||||
private val params: Params,
|
private val params: Params,
|
||||||
private val getObjectTypes: GetObjectTypes,
|
private val getObjectTypes: GetObjectTypes,
|
||||||
private val spaceManager: SpaceManager,
|
private val spaceManager: SpaceManager,
|
||||||
private val addObjectToSpace: AddObjectToSpace
|
private val addObjectToSpace: AddObjectToSpace,
|
||||||
|
private val setPinnedObjectTypes: SetPinnedObjectTypes,
|
||||||
|
private val getPinnedObjectTypes: GetPinnedObjectTypes
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(
|
override fun <T : ViewModel> create(
|
||||||
|
@ -188,7 +281,9 @@ class SelectObjectTypeViewModel(
|
||||||
params = params,
|
params = params,
|
||||||
getObjectTypes = getObjectTypes,
|
getObjectTypes = getObjectTypes,
|
||||||
spaceManager = spaceManager,
|
spaceManager = spaceManager,
|
||||||
addObjectToSpace = addObjectToSpace
|
addObjectToSpace = addObjectToSpace,
|
||||||
|
setPinnedObjectTypes = setPinnedObjectTypes,
|
||||||
|
getPinnedObjectTypes = getPinnedObjectTypes
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +300,7 @@ sealed class SelectTypeViewState{
|
||||||
|
|
||||||
sealed class SelectTypeView {
|
sealed class SelectTypeView {
|
||||||
sealed class Section : SelectTypeView() {
|
sealed class Section : SelectTypeView() {
|
||||||
|
object Pinned : Section()
|
||||||
object Objects : Section()
|
object Objects : Section()
|
||||||
object Groups : Section()
|
object Groups : Section()
|
||||||
object Library : Section()
|
object Library : Section()
|
||||||
|
@ -215,7 +311,10 @@ sealed class SelectTypeView {
|
||||||
val typeKey: Key,
|
val typeKey: Key,
|
||||||
val name: String,
|
val name: String,
|
||||||
val icon: 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()
|
) : SelectTypeView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user