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

DROID-3367 Primitives | Object type icons, part 1 (#2156)

This commit is contained in:
Konstantin Ivanov 2025-03-19 10:11:06 +01:00 committed by GitHub
parent f952730bfa
commit fc0386b4ef
Signed by: github
GPG key ID: B5690EEEBB952194
46 changed files with 837 additions and 561 deletions

View file

@ -6,6 +6,7 @@ import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.launch.GetDefaultObjectType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.spaces.AddObjectTypeToSpace
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.objects.ObjectTypeChangeViewModelFactory
@ -44,14 +45,16 @@ object ObjectTypeChangeModule {
addObjectTypeToSpace: AddObjectTypeToSpace,
dispatchers: AppCoroutineDispatchers,
spaceManager: SpaceManager,
getDefaultObjectType: GetDefaultObjectType
getDefaultObjectType: GetDefaultObjectType,
urlBuilder: UrlBuilder
): ObjectTypeChangeViewModelFactory {
return ObjectTypeChangeViewModelFactory(
getObjectTypes = getObjectTypes,
addObjectTypeToSpace = addObjectTypeToSpace,
dispatchers = dispatchers,
spaceManager = spaceManager,
getDefaultObjectType = getDefaultObjectType
getDefaultObjectType = getDefaultObjectType,
urlBuilder = urlBuilder
)
}

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.objects.SelectObjectTypeViewModel
@ -54,4 +55,5 @@ interface SelectObjectTypeDependencies : ComponentDependencies {
fun analyticSpaceHelper(): AnalyticSpaceHelperDelegate
fun userSettingsRepository(): UserSettingsRepository
fun provideSpaceManger(): SpaceManager
fun provideUrlBuilder(): UrlBuilder
}

View file

@ -51,7 +51,6 @@ 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
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
@ -62,8 +61,10 @@ import com.anytypeio.anytype.core_ui.foundation.Toolbar
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_ui.widgets.SearchField
import com.anytypeio.anytype.emojifier.Emojifier
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.SelectTypeView
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
@ -204,7 +205,7 @@ private fun FlowRowContent(
Box {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
icon = view.icon,
onItemClicked = throttledClick(
onClick = { onTypeClicked(view) }
),
@ -434,7 +435,7 @@ private fun LazyColumnContent(
) {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
icon = view.icon,
onItemClicked = throttledClick(
onClick = {
onTypeClicked(view)
@ -459,7 +460,7 @@ private fun LazyColumnContent(
fun ObjectTypeItem(
modifier: Modifier,
name: String,
emoji: String,
icon: ObjectIcon,
isSelected: Boolean,
onItemClicked: () -> Unit,
onItemLongClicked: () -> Unit
@ -491,17 +492,11 @@ fun ObjectTypeItem(
Spacer(
modifier = Modifier.width(14.dp)
)
val uri = Emojifier.safeUri(emoji)
if (uri.isNotEmpty()) {
Image(
painter = rememberAsyncImagePainter(
Emojifier.safeUri(emoji)
),
contentDescription = "Icon from URI",
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
ListWidgetObjectIcon(
icon = icon,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = name,
style = Title2,

View file

@ -4,9 +4,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResultListener
@ -29,7 +31,10 @@ import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.feature_object_type.fields.ui.FieldsMainScreen
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiErrorState
import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState
import com.anytypeio.anytype.feature_object_type.ui.icons.ChangeIconScreen
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory
import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel
import com.anytypeio.anytype.ui.editor.EditorModalFragment
@ -52,17 +57,6 @@ class ObjectTypeFragment : BaseComposeFragment() {
private val space get() = argString(ARG_SPACE)
private val objectId get() = argString(ARG_OBJECT_ID)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener(REQUEST_KEY_PICK_EMOJI) { _, bundle ->
val res = requireNotNull(bundle.getString(RESULT_EMOJI_UNICODE))
vm.updateIcon(res)
}
setFragmentResultListener(REQUEST_KEY_REMOVE_EMOJI) { _, _ ->
vm.removeIcon()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -70,6 +64,7 @@ class ObjectTypeFragment : BaseComposeFragment() {
) = content {
MaterialTheme {
ObjectTypeScreen()
IconsPickerScreen()
ErrorScreen()
}
}
@ -86,13 +81,6 @@ class ObjectTypeFragment : BaseComposeFragment() {
Timber.e(e, "Error while exiting back from object type screen")
}
}
ObjectTypeCommand.OpenEmojiPicker -> {
runCatching {
findNavController().navigate(R.id.openEmojiPicker)
}.onFailure {
Timber.w("Error while opening emoji picker")
}
}
is ObjectTypeCommand.OpenTemplate -> {
findNavController().navigate(
@ -205,6 +193,30 @@ class ObjectTypeFragment : BaseComposeFragment() {
}
}
@Composable
private fun IconsPickerScreen() {
val uiState = vm.uiIconsPickerScreen.collectAsStateWithLifecycle().value
if (uiState is UiIconsPickerState.Visible) {
ChangeIconScreen(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
vm.onTypeEvent(TypeEvent.OnIconPickerDismiss)
},
onIconClicked = { name, color ->
vm.onTypeEvent(
TypeEvent.OnIconPickerItemClick(
iconName = name,
color = color
)
)
},
onRemoveIconClicked = {
vm.onTypeEvent(TypeEvent.OnIconPickerRemovedClick)
}
)
}
}
override fun injectDependencies() {
val params = ObjectTypeVmParams(
spaceId = SpaceId(space),

View file

@ -30,6 +30,7 @@ sealed class ObjectWrapper {
val iconEmoji: String? by default
val iconImage: String? = getSingleValue(Relations.ICON_IMAGE)
val iconOption: Double? by default
val iconName: String? by default
val coverId: String? = getSingleValue(Relations.COVER_ID)
@ -200,6 +201,9 @@ sealed class ObjectWrapper {
else -> emptyList()
}
val iconName: String? by default
val iconOption: Double? by default
val allRecommendedRelations: List<Id>
get() = recommendedRelations + recommendedFeaturedRelations + recommendedHiddenRelations + recommendedFileRelations
}

View file

@ -17,6 +17,7 @@ object Relations {
const val NAME = "name"
const val ICON_EMOJI = "iconEmoji"
const val ICON_OPTION = "iconOption"
const val ICON_NAME = "iconName"
const val ICON_IMAGE = "iconImage"
const val RELATION_FORMAT = "relationFormat"
const val IS_ARCHIVED = "isArchived"

View file

@ -14,6 +14,7 @@ import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_utils.const.MimeTypes
import com.anytypeio.anytype.presentation.objects.ObjectLayoutView
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
import com.anytypeio.anytype.presentation.sets.model.ColumnView
fun Context.drawable(
@ -309,4 +310,18 @@ fun RelativeDate.getPrettyName(
is RelativeDate.Tomorrow -> resources.getString(R.string.tomorrow)
is RelativeDate.Yesterday -> resources.getString(R.string.yesterday)
RelativeDate.Empty -> ""
}
@ColorRes
fun CustomIconColor.colorRes() = when (this) {
CustomIconColor.Gray -> R.color.glyph_active
CustomIconColor.Yellow -> R.color.palette_system_yellow
CustomIconColor.Amber -> R.color.palette_system_amber_100
CustomIconColor.Red -> R.color.palette_system_red
CustomIconColor.Pink -> R.color.palette_system_pink
CustomIconColor.Purple -> R.color.palette_system_purple
CustomIconColor.Blue -> R.color.palette_system_blue
CustomIconColor.Sky -> R.color.palette_system_sky
CustomIconColor.Teal -> R.color.palette_system_teal
CustomIconColor.Green -> R.color.palette_system_green
}

View file

@ -12,11 +12,7 @@ class ObjectTypeMenuHolder(
fun bind(item: SlashItem.ObjectType) = with(binding) {
val objectType = item.objectTypeView
ivIcon.setIcon(
emoji = objectType.emoji,
image = null,
name = objectType.name
)
ivIcon.setIcon(objectType.icon)
tvTitle.text = objectType.name
if (objectType.description.isNullOrBlank()) {
tvSubtitle.gone()

View file

@ -21,9 +21,7 @@ class ObjectTypeHolder(
icSelected.gone()
}
ivIcon.setIcon(
emoji = item.emoji,
image = null,
name = item.name
icon = item.icon
)
tvTitle.text = item.name
if (item.description.isNullOrBlank()) {
@ -40,11 +38,7 @@ class ObjectTypeHorizontalHolder(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ObjectTypeView) = with(binding) {
icon.setIcon(
emoji = item.emoji,
image = null,
name = item.name
)
icon.setIcon(item.icon)
name.text = item.name
}
}

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.extensions.getMimeIcon
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.widgets.objectIcon.AvatarIconView
import com.anytypeio.anytype.core_ui.widgets.objectIcon.CustomIconView
import com.anytypeio.anytype.core_ui.widgets.objectIcon.DeletedIconView
import com.anytypeio.anytype.core_ui.widgets.objectIcon.EmojiIconView
import com.anytypeio.anytype.core_ui.widgets.objectIcon.EmptyIconView
@ -83,6 +84,13 @@ fun ListWidgetObjectIcon(
)
}
ObjectIcon.None -> {}
is ObjectIcon.ObjectType -> {
CustomIconView(
icon = icon,
modifier = modifier,
iconSize = iconSize
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.widgets
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
@ -13,6 +14,7 @@ import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.WidgetObjectIconBinding
import com.anytypeio.anytype.core_ui.extensions.color
import com.anytypeio.anytype.core_ui.extensions.colorRes
import com.anytypeio.anytype.core_ui.extensions.drawable
import com.anytypeio.anytype.core_ui.extensions.getMimeIcon
import com.anytypeio.anytype.core_ui.extensions.setCircularShape
@ -32,6 +34,7 @@ class ObjectIconWidget @JvmOverloads constructor(
companion object {
const val DEFAULT_SIZE = 28
const val DRAWABLE_DIR = "drawable"
}
val binding = WidgetObjectIconBinding.inflate(
@ -149,23 +152,10 @@ class ObjectIconWidget @JvmOverloads constructor(
ObjectIcon.Deleted -> setDeletedIcon()
is ObjectIcon.Checkbox -> setCheckbox(icon.isChecked)
is ObjectIcon.Empty -> icon.setEmptyIcon()
is ObjectIcon.ObjectType -> setCustomIcon(icon)
}
}
fun setIcon(
emoji: String?,
image: Url?,
name: String
) {
if (emoji.isNullOrBlank() && image.isNullOrBlank()) {
setProfileInitials(name)
} else {
setEmoji(emoji)
setCircularImage(image)
}
//todo Add checkbox logic
}
fun setNonExistentIcon() {
binding.ivImage.setImageResource(R.drawable.ic_non_existent_object)
}
@ -360,6 +350,32 @@ class ObjectIconWidget @JvmOverloads constructor(
}
}
private fun setCustomIcon(icon: ObjectIcon.ObjectType) {
val resId = context.resources.getIdentifier(icon.icon.drawableResId, DRAWABLE_DIR, context.packageName)
with(binding) {
ivCheckbox.invisible()
initialContainer.invisible()
ivImage.invisible()
ivBookmark.setImageDrawable(null)
ivBookmark.gone()
emojiContainer.visible()
}
try {
if (resId != 0) {
val tint = context.getColor(icon.icon.color.colorRes())
binding.tvEmojiFallback.gone()
binding.ivEmoji.setImageResource(resId)
binding.ivEmoji.imageTintList = ColorStateList.valueOf(tint)
} else {
binding.ivEmoji.setImageDrawable(null)
binding.tvEmojiFallback.gone()
binding.tvEmojiFallback.visible()
}
} catch (e: Throwable) {
Timber.w(e, "Error while setting object type icon for")
}
}
private fun ObjectIcon.Empty.setEmptyIcon() {
val (drawable, containerBackground) = when (this) {
ObjectIcon.Empty.Bookmark -> R.drawable.ic_empty_state_link to true

View file

@ -8,6 +8,8 @@ import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.core_models.primitives.TypeKey
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.presentation.editor.cover.CoverColor
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIcon
import com.anytypeio.anytype.presentation.templates.TemplateObjectTypeView
import com.anytypeio.anytype.presentation.templates.TemplateView
import com.anytypeio.anytype.presentation.templates.TemplateView.Companion.DEFAULT_TEMPLATE_ID_BLANK
@ -50,6 +52,11 @@ fun TypeTemplatesWidgetPreview() {
TemplateObjectTypeView.Item(
type = ObjectWrapper.Type(
map = mapOf(Relations.ID to "123", Relations.NAME to "Page"),
),
icon = ObjectIcon.ObjectType(
icon = CustomIcon(
rawValue = "batteryCharging"
)
)
)
),

View file

@ -953,44 +953,26 @@ fun ObjectTypesList(
},
contentAlignment = Alignment.Center
) {
val typeIcon = item.type.iconEmoji
val (rowPaddingStart, textPaddingStart) = if (typeIcon != null) {
14.dp to 8.dp
} else {
16.dp to 0.dp
}
val (rowPaddingStart, textPaddingStart) = 14.dp to 8.dp
Row(
modifier = Modifier.padding(
start = rowPaddingStart,
start = 14.dp,
end = 16.dp
),
verticalAlignment = Alignment.CenterVertically
) {
if (typeIcon != null) {
Box(
modifier = Modifier.wrapContentSize()
) {
Image(
painter = rememberAsyncImagePainter(
Emojifier.safeUri(
typeIcon
)
),
contentDescription = "Type's icon",
modifier = Modifier
.size(18.dp)
.align(Alignment.Center),
alignment = Alignment.Center
)
}
}
ListWidgetObjectIcon(
modifier = Modifier.size(20.dp),
icon = item.icon
)
Text(
text = item.type.name.orEmpty(),
style = BodyCalloutMedium.copy(
color = colorResource(id = R.color.text_primary)
),
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = textPaddingStart)
.padding(start = 8.dp)
.widthIn(max = 100.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis

View file

@ -0,0 +1,63 @@
package com.anytypeio.anytype.core_ui.widgets.objectIcon
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.colorRes
import com.anytypeio.anytype.core_ui.widgets.objectIcon.custom_icons.CustomIcons
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
import com.anytypeio.anytype.core_ui.R
@Composable
fun CustomIconView(
modifier: Modifier = Modifier,
icon: ObjectIcon.ObjectType,
iconSize: Dp
) {
val tint = colorResource(id = icon.icon.color.colorRes())
val imageVector = CustomIcons.getImageVector(icon.icon)
Box(modifier = modifier) {
if (imageVector != null) {
Image(
modifier = Modifier.size(iconSize),
imageVector = imageVector,
contentDescription = "Object Type icon",
colorFilter = ColorFilter.tint(tint),
)
} else {
Image(
modifier = Modifier.size(iconSize),
painter = painterResource(id = R.drawable.ic_empty_state_page),
contentDescription = "Object Type icon",
colorFilter = ColorFilter.tint(tint),
)
}
}
}
@Composable
@DefaultPreviews
fun CustomIconViewPreview() {
CustomIconView(
icon = ObjectIcon.ObjectType(
icon = CustomIcon(
rawValue = "batteryCharging",
color = CustomIconColor.Yellow
),
),
modifier = Modifier,
iconSize = 18.dp
)
}

View file

@ -1,405 +1,20 @@
package com.anytypeio.anytype.core_ui.widgets.objectIcon.custom_icons
import androidx.compose.ui.graphics.vector.ImageVector
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIcon
object CustomIcons {
fun getIconByName(name: String): ImageVector? = iconsMap[name]
fun getImageVector(icon: CustomIcon): ImageVector? {
return iconsMap[icon.rawValue]
}
fun getImageVector(name: String): ImageVector? {
return iconsMap[name]
}
// Маппинг, который связывает строковое название с соответствующей иконкой.
// Например, если у нас raw-имя иконки "walk" соответствует CustomIcons.CiWalk.
val iconsMap: Map<String, ImageVector> by lazy {
mapOf(
"accessibility" to CiAccessibility,
"addCircle" to CiAddCircle,
"airplane" to CiAirplane,
"alarm" to CiAlarm,
"albums" to CiAlbums,
"alertCircle" to CiAlertCircle,
"americanFootball" to CiAmericanFootball,
"analytics" to CiAnalytics,
"aperture" to CiAperture,
"apps" to CiApps,
"archive" to CiArchive,
"arrowBackCircle" to CiArrowBackCircle,
"arrowDownCircle" to CiArrowDownCircle,
"arrowForwardCircle" to CiArrowForwardCircle,
"arrowRedo" to CiArrowRedo,
"arrowRedoCircle" to CiArrowRedoCircle,
"arrowUndo" to CiArrowUndo,
"arrowUndoCircle" to CiArrowUndoCircle,
"arrowUpCircle" to CiArrowUpCircle,
"atCircle" to CiAtCircle,
"attach" to CiAttach,
"backspace" to CiBackspace,
"bag" to CiBag,
"bagAdd" to CiBagAdd,
"bagCheck" to CiBagCheck,
"bagHandle" to CiBagHandle,
"bagRemove" to CiBagRemove,
"balloon" to CiBalloon,
"ban" to CiBan,
"bandage" to CiBandage,
"barChart" to CiBarChart,
"barbell" to CiBarbell,
"barcode" to CiBarcode,
"baseball" to CiBaseball,
"basket" to CiBasket,
"basketball" to CiBasketball,
"batteryCharging" to CiBatteryCharging,
"batteryDead" to CiBatteryDead,
"batteryFull" to CiBatteryFull,
"batteryHalf" to CiBatteryHalf,
"beaker" to CiBeaker,
"bed" to CiBed,
"beer" to CiBeer,
"bicycle" to CiBicycle,
"binoculars" to CiBinoculars,
"bluetooth" to CiBluetooth,
"boat" to CiBoat,
"body" to CiBody,
"bonfire" to CiBonfire,
"book" to CiBook,
"bookmark" to CiBookmark,
"bookmarks" to CiBookmarks,
"bowlingBall" to CiBowlingBall,
"briefcase" to CiBriefcase,
"browsers" to CiBrowsers,
"brush" to CiBrush,
"bug" to CiBug,
"build" to CiBuild,
"bulb" to CiBulb,
"bus" to CiBus,
"business" to CiBusiness,
"cafe" to CiCafe,
"calculator" to CiCalculator,
"calendar" to CiCalendar,
"calendarClear" to CiCalendarClear,
"calendarNumber" to CiCalendarNumber,
"call" to CiCall,
"camera" to CiCamera,
"cameraReverse" to CiCameraReverse,
"car" to CiCar,
"carSport" to CiCarSport,
"card" to CiCard,
"caretBack" to CiCaretBack,
"caretBackCircle" to CiCaretBackCircle,
"caretDown" to CiCaretDown,
"caretDownCircle" to CiCaretDownCircle,
"caretForward" to CiCaretForward,
"caretForwardCircle" to CiCaretForwardCircle,
"caretUp" to CiCaretUp,
"caretUpCircle" to CiCaretUpCircle,
"cart" to CiCart,
"cash" to CiCash,
"cellular" to CiCellular,
"chatbox" to CiChatbox,
"chatboxEllipses" to CiChatboxEllipses,
"chatbubble" to CiChatbubble,
"chatbubbleEllipses" to CiChatbubbleEllipses,
"chatbubbles" to CiChatbubbles,
"checkbox" to CiCheckbox,
"checkmarkCircle" to CiCheckmarkCircle,
"checkmarkDoneCircle" to CiCheckmarkDoneCircle,
"chevronBackCircle" to CiChevronBackCircle,
"chevronDownCircle" to CiChevronDownCircle,
"chevronForwardCircle" to CiChevronForwardCircle,
"chevronUpCircle" to CiChevronUpCircle,
"clipboard" to CiClipboard,
"closeCircle" to CiCloseCircle,
"cloud" to CiCloud,
"cloudCircle" to CiCloudCircle,
"cloudDone" to CiCloudDone,
"cloudDownload" to CiCloudDownload,
"cloudOffline" to CiCloudOffline,
"cloudUpload" to CiCloudUpload,
"cloudy" to CiCloudy,
"cloudyNight" to CiCloudyNight,
"code" to CiCode,
"codeSlash" to CiCodeSlash,
"cog" to CiCog,
"colorFill" to CiColorFill,
"colorFilter" to CiColorFilter,
"colorPalette" to CiColorPalette,
"colorWand" to CiColorWand,
"compass" to CiCompass,
"construct" to CiConstruct,
"contract" to CiContract,
"contrast" to CiContrast,
"copy" to CiCopy,
"create" to CiCreate,
"crop" to CiCrop,
"cube" to CiCube,
"cut" to CiCut,
"desktop" to CiDesktop,
"diamond" to CiDiamond,
"dice" to CiDice,
"disc" to CiDisc,
"document" to CiDocument,
"documentAttach" to CiDocumentAttach,
"documentLock" to CiDocumentLock,
"documentText" to CiDocumentText,
"documents" to CiDocuments,
"download" to CiDownload,
"duplicate" to CiDuplicate,
"ear" to CiEar,
"earth" to CiEarth,
"easel" to CiEasel,
"egg" to CiEgg,
"ellipse" to CiEllipse,
"ellipsisHorizontalCircle" to CiEllipsisHorizontalCircle,
"ellipsisVerticalCircle" to CiEllipsisVerticalCircle,
"enter" to CiEnter,
"exit" to CiExit,
"expand" to CiExpand,
"extensionPuzzle" to CiExtensionPuzzle,
"eye" to CiEye,
"eyeOff" to CiEyeOff,
"eyedrop" to CiEyedrop,
"fastFood" to CiFastFood,
"female" to CiFemale,
"fileTray" to CiFileTray,
"fileTrayFull" to CiFileTrayFull,
"fileTrayStacked" to CiFileTrayStacked,
"film" to CiFilm,
"filterCircle" to CiFilterCircle,
"fingerPrint" to CiFingerPrint,
"fish" to CiFish,
"fitness" to CiFitness,
"flag" to CiFlag,
"flame" to CiFlame,
"flash" to CiFlash,
"flashOff" to CiFlashOff,
"flashlight" to CiFlashlight,
"flask" to CiFlask,
"flower" to CiFlower,
"folder" to CiFolder,
"folderOpen" to CiFolderOpen,
"football" to CiFootball,
"footsteps" to CiFootsteps,
"funnel" to CiFunnel,
"gameController" to CiGameController,
"gift" to CiGift,
"gitBranch" to CiGitBranch,
"gitCommit" to CiGitCommit,
"gitCompare" to CiGitCompare,
"gitMerge" to CiGitMerge,
"gitNetwork" to CiGitNetwork,
"gitPullRequest" to CiGitPullRequest,
"glasses" to CiGlasses,
"globe" to CiGlobe,
"golf" to CiGolf,
"grid" to CiGrid,
"hammer" to CiHammer,
"handLeft" to CiHandLeft,
"handRight" to CiHandRight,
"happy" to CiHappy,
"hardwareChip" to CiHardwareChip,
"headset" to CiHeadset,
"heart" to CiHeart,
"heartCircle" to CiHeartCircle,
"heartDislike" to CiHeartDislike,
"heartDislikeCircle" to CiHeartDislikeCircle,
"heartHalf" to CiHeartHalf,
"helpBuoy" to CiHelpBuoy,
"helpCircle" to CiHelpCircle,
"home" to CiHome,
"hourglass" to CiHourglass,
"iceCream" to CiIceCream,
"idCard" to CiIdCard,
"image" to CiImage,
"images" to CiImages,
"infinite" to CiInfinite,
"informationCircle" to CiInformationCircle,
"invertMode" to CiInvertMode,
"journal" to CiJournal,
"key" to CiKey,
"keypad" to CiKeypad,
"language" to CiLanguage,
"laptop" to CiLaptop,
"layers" to CiLayers,
"leaf" to CiLeaf,
"library" to CiLibrary,
"link" to CiLink,
"list" to CiList,
"listCircle" to CiListCircle,
"locate" to CiLocate,
"location" to CiLocation,
"lockClosed" to CiLockClosed,
"lockOpen" to CiLockOpen,
"logIn" to CiLogIn,
"logOut" to CiLogOut,
"logoAlipay" to CiLogoAlipay,
"logoAmazon" to CiLogoAmazon,
"logoAmplify" to CiLogoAmplify,
"logoAndroid" to CiLogoAndroid,
"magnet" to CiMagnet,
"mail" to CiMail,
"mailOpen" to CiMailOpen,
"mailUnread" to CiMailUnread,
"male" to CiMale,
"maleFemale" to CiMaleFemale,
"man" to CiMan,
"map" to CiMap,
"medal" to CiMedal,
"medical" to CiMedical,
"medkit" to CiMedkit,
"megaphone" to CiMegaphone,
"menu" to CiMenu,
"mic" to CiMic,
"micCircle" to CiMicCircle,
"micOff" to CiMicOff,
"micOffCircle" to CiMicOffCircle,
"moon" to CiMoon,
"move" to CiMove,
"musicalNote" to CiMusicalNote,
"musicalNotes" to CiMusicalNotes,
"navigate" to CiNavigate,
"navigateCircle" to CiNavigateCircle,
"newspaper" to CiNewspaper,
"notifications" to CiNotifications,
"notificationsCircle" to CiNotificationsCircle,
"notificationsOff" to CiNotificationsOff,
"notificationsOffCircle" to CiNotificationsOffCircle,
"nuclear" to CiNuclear,
"nutrition" to CiNutrition,
"options" to CiOptions,
"paperPlane" to CiPaperPlane,
"partlySunny" to CiPartlySunny,
"pause" to CiPause,
"pauseCircle" to CiPauseCircle,
"paw" to CiPaw,
"pencil" to CiPencil,
"people" to CiPeople,
"peopleCircle" to CiPeopleCircle,
"person" to CiPerson,
"personAdd" to CiPersonAdd,
"personCircle" to CiPersonCircle,
"personRemove" to CiPersonRemove,
"phoneLandscape" to CiPhoneLandscape,
"phonePortrait" to CiPhonePortrait,
"pieChart" to CiPieChart,
"pin" to CiPin,
"pint" to CiPint,
"pizza" to CiPizza,
"planet" to CiPlanet,
"play" to CiPlay,
"playBack" to CiPlayBack,
"playBackCircle" to CiPlayBackCircle,
"playCircle" to CiPlayCircle,
"playForward" to CiPlayForward,
"playForwardCircle" to CiPlayForwardCircle,
"playSkipBack" to CiPlaySkipBack,
"playSkipBackCircle" to CiPlaySkipBackCircle,
"playSkipForward" to CiPlaySkipForward,
"playSkipForwardCircle" to CiPlaySkipForwardCircle,
"podium" to CiPodium,
"power" to CiPower,
"pricetag" to CiPricetag,
"pricetags" to CiPricetags,
"print" to CiPrint,
"prism" to CiPrism,
"pulse" to CiPulse,
"push" to CiPush,
"qrCode" to CiQrCode,
"radio" to CiRadio,
"radioButtonOff" to CiRadioButtonOff,
"radioButtonOn" to CiRadioButtonOn,
"rainy" to CiRainy,
"reader" to CiReader,
"receipt" to CiReceipt,
"recording" to CiRecording,
"refresh" to CiRefresh,
"refreshCircle" to CiRefreshCircle,
"reload" to CiReload,
"reloadCircle" to CiReloadCircle,
"removeCircle" to CiRemoveCircle,
"repeat" to CiRepeat,
"resize" to CiResize,
"restaurant" to CiRestaurant,
"ribbon" to CiRibbon,
"rocket" to CiRocket,
"rose" to CiRose,
"sad" to CiSad,
"save" to CiSave,
"scale" to CiScale,
"scan" to CiScan,
"scanCircle" to CiScanCircle,
"school" to CiSchool,
"search" to CiSearch,
"searchCircle" to CiSearchCircle,
"send" to CiSend,
"server" to CiServer,
"settings" to CiSettings,
"shapes" to CiShapes,
"share" to CiShare,
"shareSocial" to CiShareSocial,
"shield" to CiShield,
"shieldCheckmark" to CiShieldCheckmark,
"shieldHalf" to CiShieldHalf,
"shirt" to CiShirt,
"shuffle" to CiShuffle,
"skull" to CiSkull,
"snow" to CiSnow,
"sparkles" to CiSparkles,
"speedometer" to CiSpeedometer,
"square" to CiSquare,
"star" to CiStar,
"starHalf" to CiStarHalf,
"statsChart" to CiStatsChart,
"stop" to CiStop,
"stopCircle" to CiStopCircle,
"stopwatch" to CiStopwatch,
"storefront" to CiStorefront,
"subway" to CiSubway,
"sunny" to CiSunny,
"swapHorizontal" to CiSwapHorizontal,
"swapVertical" to CiSwapVertical,
"sync" to CiSync,
"syncCircle" to CiSyncCircle,
"tabletLandscape" to CiTabletLandscape,
"tabletPortrait" to CiTabletPortrait,
"telescope" to CiTelescope,
"tennisball" to CiTennisball,
"terminal" to CiTerminal,
"text" to CiText,
"thermometer" to CiThermometer,
"thumbsDown" to CiThumbsDown,
"thumbsUp" to CiThumbsUp,
"thunderstorm" to CiThunderstorm,
"ticket" to CiTicket,
"time" to CiTime,
"timer" to CiTimer,
"today" to CiToday,
"toggle" to CiToggle,
"trailSign" to CiTrailSign,
"train" to CiTrain,
"transgender" to CiTransgender,
"trash" to CiTrash,
"trashBin" to CiTrashBin,
"trendingDown" to CiTrendingDown,
"trendingUp" to CiTrendingUp,
"triangle" to CiTriangle,
"trophy" to CiTrophy,
"tv" to CiTv,
"umbrella" to CiUmbrella,
"unlink" to CiUnlink,
"videocam" to CiVideocam,
"videocamOff" to CiVideocamOff,
"volumeHigh" to CiVolumeHigh,
"volumeLow" to CiVolumeLow,
"volumeMedium" to CiVolumeMedium,
"volumeMute" to CiVolumeMute,
"volumeOff" to CiVolumeOff,
"walk" to CiWalk,
"wallet" to CiWallet,
"warning" to CiWarning,
"watch" to CiWatch,
"water" to CiWater,
"wifi" to CiWifi,
"wine" to CiWine,
"woman" to CiWoman
)
mapOf()
}
}

View file

@ -30,14 +30,14 @@ 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 coil.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.emojifier.Emojifier
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModel.TypesWidgetItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
@ -50,11 +50,11 @@ fun MyChooseTypeHorizontalWidget() {
TypesWidgetItem.Search,
TypesWidgetItem.Type(
item = ObjectTypeView(
emoji = "👍",
name = "Like",
id = "12312",
description = null,
key = "dd"
key = "dd",
icon = ObjectIcon.None
)
)
),
@ -72,11 +72,11 @@ fun MyChooseTypeHorizontalWidgetCollapsed() {
TypesWidgetItem.Search,
TypesWidgetItem.Type(
item = ObjectTypeView(
emoji = "👍",
name = "Like",
id = "12312",
description = null,
key = "dd"
key = "dd",
icon = ObjectIcon.None
)
)
),
@ -158,15 +158,11 @@ fun ChooseTypeHorizontalWidgetExpanded(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(12.dp))
val uri = Emojifier.safeUri(item.item.emoji.orEmpty())
if (uri.isNotEmpty()) {
Image(
painter = rememberAsyncImagePainter(uri),
contentDescription = "Icon from URI",
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
ListWidgetObjectIcon(
modifier = Modifier.size(16.dp),
icon = item.item.icon
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = item.item.name,
style = Caption1Medium,

View file

@ -108,6 +108,8 @@ class ObjectTypesSubscriptionManager (
Relations.LAYOUT,
Relations.DESCRIPTION,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.SOURCE_OBJECT,
Relations.IS_READ_ONLY,
Relations.RECOMMENDED_LAYOUT,

View file

@ -58,6 +58,7 @@ interface ProfileSubscriptionManager : GlobalSubscription {
Relations.ID,
Relations.NAME,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_IMAGE,
Relations.ICON_OPTION,
Relations.SHARED_SPACES_LIMIT

View file

@ -74,6 +74,7 @@ class GetTemplates(
Relations.NAME,
Relations.LAYOUT,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_IMAGE,
Relations.ICON_OPTION,
Relations.COVER_ID,

View file

@ -214,14 +214,14 @@ fun ObjectWrapper.Basic.toAllContentItem(
)
}
fun List<ObjectWrapper.Basic>.toUiContentTypes(
fun List<ObjectWrapper.Type>.toUiContentTypes(
urlBuilder: UrlBuilder,
isOwnerOrEditor: Boolean
): List<UiContentItem.Type> {
return map { it.toAllContentType(urlBuilder, isOwnerOrEditor) }
}
fun ObjectWrapper.Basic.toAllContentType(
fun ObjectWrapper.Type.toAllContentType(
urlBuilder: UrlBuilder,
isOwnerOrEditor: Boolean
): UiContentItem.Type {

View file

@ -316,10 +316,12 @@ class AllContentViewModel(
val isOwnerOrEditor = permission.value?.isOwnerOrEditor() == true
return when (activeTab) {
AllContentTab.TYPES -> {
val items = objectWrappers.toUiContentTypes(
urlBuilder = urlBuilder,
isOwnerOrEditor = isOwnerOrEditor
)
val items = objectWrappers
.map { ObjectWrapper.Type(it.map) }
.toUiContentTypes(
urlBuilder = urlBuilder,
isOwnerOrEditor = isOwnerOrEditor
)
buildList {
if (isOwnerOrEditor) add(UiContentItem.NewType)
addAll(items)
@ -346,11 +348,19 @@ class AllContentViewModel(
}
val result = when (activeSort) {
is ObjectsListSort.ByDateCreated -> {
groupItemsByDate(items = items, isSortByDateCreated = true, activeSort = activeSort)
groupItemsByDate(
items = items,
isSortByDateCreated = true,
activeSort = activeSort
)
}
is ObjectsListSort.ByDateUpdated -> {
groupItemsByDate(items = items, isSortByDateCreated = false, activeSort = activeSort)
groupItemsByDate(
items = items,
isSortByDateCreated = false,
activeSort = activeSort
)
}
is ObjectsListSort.ByName -> {

View file

@ -2,6 +2,8 @@ package com.anytypeio.anytype.feature_object_type.ui
import com.anytypeio.anytype.core_models.ObjectType.Layout
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
import com.anytypeio.anytype.presentation.templates.TemplateView
sealed class TypeEvent {
@ -42,4 +44,10 @@ sealed class TypeEvent {
data object OnLayoutButtonClick : TypeEvent()
data object OnFieldsButtonClick : TypeEvent()
data object OnTemplatesButtonClick : TypeEvent()
//region Icon picker
data class OnIconPickerItemClick(val iconName: String, val color: CustomIconColor?) : TypeEvent()
data object OnIconPickerRemovedClick : TypeEvent()
data object OnIconPickerDismiss : TypeEvent()
//endregion
}

View file

@ -29,8 +29,6 @@ sealed class ObjectTypeCommand {
val spaceId: Id
) : ObjectTypeCommand()
data object OpenEmojiPicker : ObjectTypeCommand()
data object OpenFieldsScreen : ObjectTypeCommand()
data class OpenEditTypePropertiesScreen(val typeId: Id, val space: Id) : ObjectTypeCommand()
@ -230,4 +228,12 @@ sealed class UiSyncStatusBadgeState {
data object Hidden : UiSyncStatusBadgeState()
data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState()
}
//endregion
//endregion
//region Type icon screen
sealed class UiIconsPickerState {
data object Hidden : UiIconsPickerState()
data object Visible : UiIconsPickerState()
}
//endregion

View file

@ -0,0 +1,307 @@
package com.anytypeio.anytype.feature_object_type.ui.icons
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedback
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedbackType
import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback
import com.anytypeio.anytype.core_ui.extensions.colorRes
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.widgets.objectIcon.custom_icons.CustomIcons
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.icons.ChangeIconScreenConst.secondRowColors
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChangeIconScreen(
modifier: Modifier,
onDismissRequest: () -> Unit,
onIconClicked: (String, CustomIconColor?) -> Unit,
onRemoveIconClicked: () -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
val allIconNames = remember { CustomIcons.iconsMap.keys.toList() }
ModalBottomSheet(
modifier = modifier.windowInsetsPadding(WindowInsets.statusBars),
dragHandle = {
Column {
Spacer(modifier = Modifier.height(6.dp))
Dragger()
Spacer(modifier = Modifier.height(6.dp))
}
},
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_secondary),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetState = bottomSheetState,
onDismissRequest = onDismissRequest
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(R.string.object_type_icon_change_title),
style = Title1,
color = colorResource(R.color.text_primary),
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp)
.noRippleThrottledClickable {
onRemoveIconClicked()
},
text = stringResource(R.string.object_type_icon_remove),
style = BodyRegular,
color = colorResource(R.color.palette_system_red),
textAlign = TextAlign.Center
)
}
var searchQuery by remember { mutableStateOf("") }
DefaultSearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
hint = R.string.object_type_icon_change_title_search_hint
) { newQuery ->
searchQuery = newQuery
}
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
Spacer(modifier = Modifier.height(16.dp))
val filteredIcons = if (searchQuery.isEmpty()) {
allIconNames
} else {
allIconNames.filter { it.contains(searchQuery, ignoreCase = true) }
}
IconSelectionGrid(
icons = filteredIcons,
onIconClicked = onIconClicked,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(64.dp))
}
}
@Composable
fun IconSelectionGrid(
modifier: Modifier = Modifier,
icons: List<String>,
onIconClicked: (String, CustomIconColor?) -> Unit
) {
val hapticFeedback = rememberReorderHapticFeedback()
LazyVerticalGrid(
modifier = modifier,
columns = GridCells.Adaptive(minSize = 57.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = icons,
key = { iconName -> iconName },
contentType = { "icon" }
) { iconName ->
IconItem(
modifier = Modifier.wrapContentSize(),
hapticFeedback = hapticFeedback,
iconName = iconName,
onIconClicked = onIconClicked
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun IconItem(
modifier: Modifier,
iconName: String,
hapticFeedback: ReorderHapticFeedback,
onIconClicked: (String, CustomIconColor?) -> Unit
) {
val showIconPreviews = remember { mutableStateOf(false) }
Box(modifier = modifier
.combinedClickable(
enabled = true,
onClick = {
onIconClicked(iconName, null)
},
onLongClick = {
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.START)
showIconPreviews.value = true
}
)) {
val imageVector = CustomIcons.getImageVector(iconName)
val tintColor = if (!showIconPreviews.value) {
colorResource(id = CustomIconColor.Gray.colorRes())
} else {
colorResource(id = R.color.glyph_inactive)
}
if (imageVector != null) {
Image(
modifier = Modifier
.size(40.dp)
.align(Alignment.Center),
imageVector = imageVector,
contentDescription = "Object Type icon",
colorFilter = ColorFilter.tint(tintColor),
)
IconPreviews(
imageVector = imageVector,
show = showIconPreviews.value,
onDismissRequest = { showIconPreviews.value = false },
onIconClicked = { color ->
showIconPreviews.value = false
onIconClicked(iconName, color)
}
)
}
}
}
@Composable
private fun IconPreviews(
imageVector: ImageVector,
show: Boolean,
onDismissRequest: () -> Unit,
onIconClicked: (CustomIconColor) -> Unit
) {
DropdownMenu(
modifier = Modifier
.wrapContentSize()
.padding(horizontal = 12.dp, vertical = 4.dp),
expanded = show,
onDismissRequest = onDismissRequest,
shape = RoundedCornerShape(size = 20.dp),
containerColor = colorResource(id = R.color.background_primary),
shadowElevation = 5.dp,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ChangeIconScreenConst.firstRowColors.forEach { customColor ->
val color = colorResource(id = customColor.colorRes())
Image(
modifier = Modifier
.size(40.dp)
.noRippleThrottledClickable {
onIconClicked(customColor)
},
imageVector = imageVector,
contentDescription = "Object Type icon",
colorFilter = ColorFilter.tint(color),
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
secondRowColors.forEach { customColor ->
val color = colorResource(id = customColor.colorRes())
Image(
modifier = Modifier
.size(40.dp)
.noRippleThrottledClickable {
onIconClicked(customColor)
},
imageVector = imageVector,
contentDescription = "Object Type icon",
colorFilter = ColorFilter.tint(color),
)
}
}
}
}
object ChangeIconScreenConst {
val firstRowColors = listOf(
CustomIconColor.Gray,
CustomIconColor.Yellow,
CustomIconColor.Amber,
CustomIconColor.Red,
CustomIconColor.Pink
)
val secondRowColors = listOf(
CustomIconColor.Purple,
CustomIconColor.Blue,
CustomIconColor.Sky,
CustomIconColor.Teal,
CustomIconColor.Green
)
}
@Composable
@DefaultPreviews
fun DefaultChangeIconScreenPreview() {
IconSelectionGrid(
modifier = Modifier.fillMaxSize(),
icons = CustomIcons.iconsMap.keys.toList(),
onIconClicked = { _, _ -> },
)
}

View file

@ -32,13 +32,13 @@ import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.OpenEmojiPicker
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiDeleteAlertState
import com.anytypeio.anytype.feature_object_type.ui.UiEditButton
import com.anytypeio.anytype.feature_object_type.ui.UiErrorState
import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
@ -55,6 +55,7 @@ import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState
@ -135,6 +136,9 @@ class ObjectTypeViewModel(
//edit property
val uiEditPropertyScreen = MutableStateFlow<UiEditPropertyState>(UiEditPropertyState.Hidden)
//icons picker screen
val uiIconsPickerScreen = MutableStateFlow<UiIconsPickerState>(UiIconsPickerState.Hidden)
//error
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
//endregion
@ -449,9 +453,7 @@ class ObjectTypeViewModel(
}
TypeEvent.OnObjectTypeIconClick -> {
viewModelScope.launch {
commands.emit(OpenEmojiPicker)
}
uiIconsPickerScreen.value = UiIconsPickerState.Visible
}
is TypeEvent.OnTemplateItemClick -> {
@ -489,6 +491,20 @@ class ObjectTypeViewModel(
)
}
}
TypeEvent.OnIconPickerDismiss -> {
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
}
is TypeEvent.OnIconPickerItemClick -> {
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
updateIcon(iconName = event.iconName, newColor = event.color)
}
TypeEvent.OnIconPickerRemovedClick -> {
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
removeIcon()
}
}
}
@ -584,30 +600,39 @@ class ObjectTypeViewModel(
}
}
fun updateIcon(
emoji: String
private fun updateIcon(
iconName: String,
newColor: CustomIconColor?
) {
viewModelScope.launch {
val params = SetObjectDetails.Params(
ctx = vmParams.objectId,
details = mapOf(Relations.ICON_EMOJI to emoji)
details = mapOf(
Relations.ICON_EMOJI to null,
Relations.ICON_NAME to iconName,
Relations.ICON_OPTION to newColor?.iconOption?.toDouble()
)
)
setObjectDetails.async(params).fold(
onFailure = { error ->
Timber.e(error, "Error while updating data view record")
},
onSuccess = {
Timber.d("Object type icon updated to icon: $iconName")
}
)
}
}
fun removeIcon() {
private fun removeIcon() {
viewModelScope.launch {
val params = SetObjectDetails.Params(
ctx = vmParams.objectId,
details = mapOf(Relations.ICON_EMOJI to null)
details = mapOf(
Relations.ICON_EMOJI to null,
Relations.ICON_NAME to null,
Relations.ICON_OPTION to null
)
)
setObjectDetails.async(params).fold(
onFailure = { error ->
@ -700,6 +725,7 @@ class ObjectTypeViewModel(
currentList.add(toIndex, item)
uiFieldsListState.value = UiFieldsListState(items = currentList)
}
is FieldEvent.EditProperty -> proceedWithEditPropertyEvent(event)
}
}

View file

@ -1986,4 +1986,8 @@ Please provide specific details of your needs here.</string>
<string name="property_edit_menu_unlink">Unlink from type</string>
<string name="delete_space_checkbox_text">I have read and want to delete this space</string>
<string name="object_type_icon_change_title">Change icon</string>
<string name="object_type_icon_remove">Remove</string>
<string name="object_type_icon_change_title_search_hint">Search..."</string>
</resources>

View file

@ -5281,7 +5281,8 @@ class EditorViewModel(
isWithCollection = false,
isWithBookmark = false,
selectedTypes = emptyList(),
excludeTypes = emptyList()
excludeTypes = emptyList(),
urlBuilder = urlBuilder
)
action.invoke(views)
}
@ -6417,7 +6418,8 @@ class EditorViewModel(
objects.getObjectTypeViewsForSBPage(
isWithCollection = true,
isWithBookmark = false,
excludeTypes = excludeTypes
excludeTypes = excludeTypes,
urlBuilder = urlBuilder
).filter {
!excludeTypes.contains(it.key)
}.map {

View file

@ -724,23 +724,27 @@ fun ColumnView.Format.toRelationFormat(): RelationFormat = when (this) {
ColumnView.Format.UNDEFINED -> RelationFormat.UNDEFINED
}
fun ObjectWrapper.Type.toObjectTypeView(selectedSources: List<Id> = emptyList()): ObjectTypeView =
fun ObjectWrapper.Type.toObjectTypeView(
selectedSources: List<Id> = emptyList(),
urlBuilder: UrlBuilder
): ObjectTypeView =
ObjectTypeView(
id = id,
key = uniqueKey,
name = name.orEmpty(),
emoji = iconEmoji,
description = description,
isSelected = selectedSources.contains(id),
defaultTemplate = defaultTemplateId,
sourceObject = sourceObject
sourceObject = sourceObject,
icon = objectIcon(urlBuilder)
)
fun List<ObjectWrapper.Type>.toTemplateObjectTypeViewItems(selectedType: Id): List<TemplateObjectTypeView.Item> {
fun List<ObjectWrapper.Type>.toTemplateObjectTypeViewItems(selectedType: Id, urlBuilder: UrlBuilder): List<TemplateObjectTypeView.Item> {
return map {
TemplateObjectTypeView.Item(
type = it,
isSelected = it.id == selectedType
isSelected = it.id == selectedType,
icon = it.objectIcon(urlBuilder)
)
}
}

View file

@ -6,6 +6,8 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectIcon.Basic
import com.anytypeio.anytype.core_models.SupportedLayouts
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
fun ObjectWrapper.Basic.objectIcon(builder: UrlBuilder): ObjectIcon {
@ -17,7 +19,9 @@ fun ObjectWrapper.Basic.objectIcon(builder: UrlBuilder): ObjectIcon {
image = iconImage,
emoji = iconEmoji,
builder = builder,
name = name.orEmpty()
name = name.orEmpty(),
iconName = iconName,
iconOption = iconOption?.toInt()
)
if (objectIcon != null) {
@ -49,7 +53,9 @@ fun ObjectWrapper.Type.objectIcon(builder: UrlBuilder): ObjectIcon {
image = null,
emoji = iconEmoji,
builder = builder,
name = name.orEmpty()
name = name.orEmpty(),
iconName = iconName,
iconOption = iconOption?.toInt()
)
if (objectIcon != null) {
@ -76,16 +82,22 @@ fun ObjectType.Layout?.emptyType(): ObjectIcon.Empty {
fun ObjectType.Layout.icon(
image: String?,
emoji: String?,
iconName: String?,
iconOption: Int?,
name: String,
builder: UrlBuilder
): ObjectIcon? {
return when (this) {
ObjectType.Layout.OBJECT_TYPE -> handleObjectTypeIcon(
emoji = emoji,
iconName = iconName,
iconOption = iconOption
)
ObjectType.Layout.BASIC,
ObjectType.Layout.SET,
ObjectType.Layout.COLLECTION,
ObjectType.Layout.IMAGE,
ObjectType.Layout.OBJECT_TYPE -> basicIcon(
ObjectType.Layout.IMAGE -> basicIcon(
image = image,
emoji = emoji,
builder = builder,
@ -110,6 +122,31 @@ fun ObjectType.Layout.icon(
}
}
/**
* Handles icons for OBJECT_TYPE layout.
*/
private fun handleObjectTypeIcon(
emoji: String?,
iconName: String?,
iconOption: Int?,
): ObjectIcon? {
return when {
!emoji.isNullOrEmpty() -> {
Basic.Emoji(
unicode = emoji,
emptyState = ObjectType.Layout.OBJECT_TYPE.emptyType()
)
}
iconName.isNullOrEmpty() -> ObjectIcon.Empty.ObjectType
else -> ObjectIcon.ObjectType(
icon = CustomIcon(
rawValue = iconName,
color = CustomIconColor.fromIconOption(iconOption)
),
)
}
}
private fun basicIcon(
image: String?,
emoji: String?,

View file

@ -4,6 +4,8 @@ import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIcon
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
sealed class ObjectIcon {
@ -41,6 +43,7 @@ sealed class ObjectIcon {
data object Deleted : ObjectIcon()
data class Checkbox(val isChecked: Boolean) : ObjectIcon()
data class ObjectType(val icon: CustomIcon) : ObjectIcon()
}
sealed class SpaceMemberIconView {

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.launch.GetDefaultObjectType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.spaces.AddObjectToSpace
import com.anytypeio.anytype.domain.spaces.AddObjectTypeToSpace
import com.anytypeio.anytype.domain.workspace.SpaceManager
@ -42,6 +43,7 @@ class ObjectTypeChangeViewModel(
private val dispatchers: AppCoroutineDispatchers,
private val spaceManager: SpaceManager,
private val getDefaultObjectType: GetDefaultObjectType,
private val urlBuilder: UrlBuilder
) : BaseViewModel() {
private val userInput = MutableStateFlow(DEFAULT_INPUT)
@ -219,7 +221,8 @@ class ObjectTypeChangeViewModel(
isWithBookmark = setup.isWithBookmark,
excludeTypes = setup.excludeTypes,
selectedTypes = setup.selectedTypes,
useCustomComparator = false
useCustomComparator = false,
urlBuilder = urlBuilder
).map {
ObjectTypeItemView.Type(it)
}
@ -234,7 +237,8 @@ class ObjectTypeChangeViewModel(
isWithBookmark = setup.isWithBookmark,
excludeTypes = setup.excludeTypes,
selectedTypes = setup.selectedTypes,
useCustomComparator = false
useCustomComparator = false,
urlBuilder = urlBuilder
).map {
ObjectTypeItemView.Type(it)
}

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.launch.GetDefaultObjectType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.spaces.AddObjectTypeToSpace
import com.anytypeio.anytype.domain.workspace.SpaceManager
@ -13,7 +14,8 @@ class ObjectTypeChangeViewModelFactory(
private val addObjectTypeToSpace: AddObjectTypeToSpace,
private val dispatchers: AppCoroutineDispatchers,
private val spaceManager: SpaceManager,
private val getDefaultObjectType: GetDefaultObjectType
private val getDefaultObjectType: GetDefaultObjectType,
private val urlBuilder: UrlBuilder
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -23,7 +25,8 @@ class ObjectTypeChangeViewModelFactory(
addObjectTypeToSpace = addObjectTypeToSpace,
dispatchers = dispatchers,
spaceManager = spaceManager,
getDefaultObjectType = getDefaultObjectType
getDefaultObjectType = getDefaultObjectType,
urlBuilder = urlBuilder
) as T
}
}

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.presentation.mapper.toObjectTypeView
import com.anytypeio.anytype.core_models.SupportedLayouts.editorLayouts
import com.anytypeio.anytype.core_models.SupportedLayouts.fileLayouts
import com.anytypeio.anytype.core_models.SupportedLayouts.systemLayouts
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.sets.state.ObjectState
/**
@ -34,7 +35,8 @@ fun List<ObjectWrapper.Type>.getObjectTypeViewsForSBPage(
isWithBookmark: Boolean = false,
excludeTypes: List<String> = emptyList(),
selectedTypes: List<String> = emptyList(),
useCustomComparator: Boolean = true
useCustomComparator: Boolean = true,
urlBuilder: UrlBuilder
): List<ObjectTypeView> {
val result = mutableListOf<ObjectTypeView>()
forEach { obj ->
@ -43,14 +45,14 @@ fun List<ObjectWrapper.Type>.getObjectTypeViewsForSBPage(
}
if (obj.uniqueKey == COLLECTION || obj.uniqueKey == SET) {
if (isWithCollection) {
val objTypeView = obj.toObjectTypeView(selectedTypes)
val objTypeView = obj.toObjectTypeView(selectedTypes, urlBuilder)
result.add(objTypeView)
}
return@forEach
}
if (obj.uniqueKey == BOOKMARK) {
if (isWithBookmark) {
val objTypeView = obj.toObjectTypeView(selectedTypes)
val objTypeView = obj.toObjectTypeView(selectedTypes, urlBuilder)
result.add(objTypeView)
}
return@forEach
@ -58,7 +60,7 @@ fun List<ObjectWrapper.Type>.getObjectTypeViewsForSBPage(
if (excludeTypes.contains(obj.id)) {
return@forEach
}
val objTypeView = obj.toObjectTypeView(selectedTypes)
val objTypeView = obj.toObjectTypeView(selectedTypes, urlBuilder)
result.add(objTypeView)
return@forEach
}

View file

@ -21,10 +21,10 @@ data class ObjectTypeView(
val key: Key,
val name: String,
val description: String?,
val emoji: String?,
val isSelected: Boolean = false,
val defaultTemplate: Id? = null,
val sourceObject: Id? = null
val sourceObject: Id? = null,
val icon: ObjectIcon
)
class ObjectTypeViewComparator : Comparator<ObjectTypeView> {

View file

@ -26,6 +26,7 @@ import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.launch.GetDefaultObjectType
import com.anytypeio.anytype.domain.launch.SetDefaultObjectType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.CreateBookmarkObject
import com.anytypeio.anytype.domain.objects.CreatePrefilledNote
import com.anytypeio.anytype.domain.spaces.AddObjectToSpace
@ -35,6 +36,7 @@ import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@ -57,7 +59,8 @@ class SelectObjectTypeViewModel(
private val createBookmarkObject: CreateBookmarkObject,
private val createPrefilledNote: CreatePrefilledNote,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val urlBuilder: UrlBuilder
) : BaseViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
val viewState = MutableStateFlow<SelectTypeViewState>(SelectTypeViewState.Loading)
@ -133,7 +136,7 @@ class SelectObjectTypeViewModel(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
icon = type.objectIcon(urlBuilder),
isPinned = true,
isFirstInSection = index == 0,
isLastInSection = index == pinnedTypes.lastIndex,
@ -152,7 +155,7 @@ class SelectObjectTypeViewModel(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
icon = type.objectIcon(urlBuilder),
isFirstInSection = index == 0,
isLastInSection = index == pinnedTypes.lastIndex,
isPinnable = true,
@ -172,7 +175,7 @@ class SelectObjectTypeViewModel(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
icon = type.objectIcon(urlBuilder),
isPinnable = true,
isFirstInSection = index == 0,
isLastInSection = index == pinnedTypes.lastIndex,
@ -190,7 +193,7 @@ class SelectObjectTypeViewModel(
id = type.id,
typeKey = type.uniqueKey,
name = type.name.orEmpty(),
icon = type.iconEmoji.orEmpty(),
icon = type.objectIcon(urlBuilder),
isFromLibrary = true,
isPinned = false,
isPinnable = false,
@ -464,7 +467,8 @@ class SelectObjectTypeViewModel(
private val createBookmarkObject: CreateBookmarkObject,
private val createPrefilledNote: CreatePrefilledNote,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val urlBuilder: UrlBuilder
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
@ -480,7 +484,8 @@ class SelectObjectTypeViewModel(
createBookmarkObject = createBookmarkObject,
createPrefilledNote = createPrefilledNote,
analytics = analytics,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
urlBuilder = urlBuilder
) as T
}
@ -515,7 +520,7 @@ sealed class SelectTypeView {
val id: Id,
val typeKey: Key,
val name: String,
val icon: String,
val icon: ObjectIcon,
val isFromLibrary: Boolean = false,
val isPinned: Boolean = false,
val isFirstInSection: Boolean = false,

View file

@ -0,0 +1,34 @@
package com.anytypeio.anytype.presentation.objects.custom_icon
/**
* Data class representing a custom icon.
* @property rawValue The raw string representing the icon.
* @property color The color of the icon, if any.
*/
data class CustomIcon(
val rawValue: String,
val color: CustomIconColor = CustomIconColor.DEFAULT
) {
/**
* Returns the drawable resource name for this icon.
*
* The drawable name is generated by converting [rawValue] from kebab-case
* (e.g., "battery-dead") to snake_case ("battery_dead") and prefixing it with "ci_".
*/
val drawableResId: String
get() = DEFAULT_ICON_PREFIX + rawValue.toSnakeCase()
companion object {
const val DEFAULT_ICON_PREFIX = "ci_"
}
}
/**
* Extension function that converts a kebab-case string to snake_case.
*
* Simply replaces all occurrences of '-' with '_'.
*
* @receiver String to be converted.
* @return The snake_case version of the string.
*/
private fun String.toSnakeCase(): String = replace("-", "_")

View file

@ -0,0 +1,61 @@
package com.anytypeio.anytype.presentation.objects.custom_icon
/**
* Enum representing custom icon colors with associated raw integer values.
*
* Each color has a corresponding [iconOption] and [id]. The [iconOption] is
* computed as the raw value plus one, while the [id] is equal to the raw value.
* The default color is specified by [DEFAULT].
*/
enum class CustomIconColor(val rawValue: Int) {
Gray(0),
Yellow(1),
Amber(2),
Red(3),
Pink(4),
Purple(5),
Blue(6),
Sky(7),
Teal(8),
Green(9);
/**
* Returns the icon option corresponding to this color.
*
* This is calculated as the [rawValue] plus one.
*/
val iconOption: Int
get() = rawValue + 1
/**
* Returns the id corresponding to this color.
*
* The id is equal to the [rawValue].
*/
val id: Int
get() = rawValue
companion object {
/**
* Default color used when a specific color is not provided.
*/
val DEFAULT: CustomIconColor = Gray
/**
* Returns the [CustomIconColor] associated with the provided [iconOption].
*
* If [iconOption] is null or does not match any [CustomIconColor],
* the [DEFAULT] color is returned.
*
* @param iconOption The icon option integer, which may be null.
* @return The matching [CustomIconColor] or [DEFAULT] if no match is found.
*/
fun fromIconOption(iconOption: Int?): CustomIconColor {
return if (iconOption == null) {
DEFAULT
} else {
CustomIconColor.entries.find { it.iconOption == iconOption } ?: DEFAULT
}
}
}
}

View file

@ -105,6 +105,8 @@ class LimitObjectTypeViewModel(
Relations.SNIPPET,
Relations.DESCRIPTION,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.ICON_IMAGE,
Relations.LAYOUT
)

View file

@ -663,6 +663,7 @@ object ObjectSearchConstants {
Relations.NAME,
Relations.ICON_IMAGE,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.TYPE,
Relations.LAYOUT,
@ -708,6 +709,8 @@ object ObjectSearchConstants {
Relations.NAME,
Relations.ICON_IMAGE,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.TYPE,
Relations.LAYOUT,
Relations.IS_ARCHIVED,
@ -733,6 +736,8 @@ object ObjectSearchConstants {
Relations.NAME,
Relations.DESCRIPTION,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.TYPE,
Relations.LAYOUT,
Relations.IS_ARCHIVED,
@ -975,6 +980,8 @@ object ObjectSearchConstants {
Relations.DESCRIPTION,
Relations.ICON_IMAGE,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.TYPE,
Relations.LAYOUT,
Relations.IS_ARCHIVED,
@ -1295,6 +1302,7 @@ object ObjectSearchConstants {
Relations.NAME,
Relations.ICON_IMAGE,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_OPTION,
Relations.CREATED_DATE,
Relations.SPACE_ACCOUNT_STATUS,

View file

@ -2230,7 +2230,7 @@ class ObjectSetViewModel(
onSuccess = { types ->
val list = buildList {
add(TemplateObjectTypeView.Search)
addAll(types.toTemplateObjectTypeViewItems(selectedType))
addAll(types.toTemplateObjectTypeViewItems(selectedType, urlBuilder))
}
typeTemplatesWidgetState.value = widgetState.copy(objectTypes = list)
},

View file

@ -78,6 +78,7 @@ class DefaultObjectTypeTemplatesContainer(
Relations.TYPE_UNIQUE_KEY,
Relations.NAME,
Relations.ICON_EMOJI,
Relations.ICON_NAME,
Relations.ICON_IMAGE,
Relations.ICON_OPTION,
Relations.COVER_ID,

View file

@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.core_models.primitives.TypeKey
import com.anytypeio.anytype.presentation.editor.cover.CoverColor
import com.anytypeio.anytype.presentation.objects.ObjectIcon
sealed class TemplateView {
@ -69,6 +70,7 @@ sealed class TemplateObjectTypeView {
data class Item(
val type: ObjectWrapper.Type,
val icon: ObjectIcon,
val isSelected: Boolean = false
) : TemplateObjectTypeView()

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.editor.editor
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.ObjectViewDetails
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.StubRelationObject
@ -14,6 +15,7 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashRelationView
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashWidgetState
import com.anytypeio.anytype.presentation.number.NumberParser
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
import com.anytypeio.anytype.presentation.relations.ObjectRelationView
import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule
@ -367,8 +369,11 @@ class EditorSlashWidgetClicksTest: EditorPresentationTestSetup() {
id = type1.id,
key = type1.uniqueKey,
name = type1.name.orEmpty(),
icon = ObjectIcon.Basic.Emoji(
unicode = type1.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
description = type1.description,
emoji = type1.iconEmoji
)
),
SlashItem.ObjectType(
@ -377,7 +382,10 @@ class EditorSlashWidgetClicksTest: EditorPresentationTestSetup() {
key = type2.uniqueKey,
name = type2.name.orEmpty(),
description = type2.description,
emoji = type2.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type2.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
),
SlashItem.ObjectType(
@ -386,7 +394,10 @@ class EditorSlashWidgetClicksTest: EditorPresentationTestSetup() {
key = type3.uniqueKey,
name = type3.name.orEmpty(),
description = type3.description,
emoji = type3.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type3.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
)
)

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashRelationView
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashWidgetState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
import com.anytypeio.anytype.presentation.relations.ObjectRelationView
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
@ -470,7 +471,10 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
key = type1.uniqueKey,
name = type1.name.orEmpty(),
description = type1.description,
emoji = type1.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type1.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
),
SlashItem.ObjectType(
@ -479,7 +483,10 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
key = type2.uniqueKey,
name = type2.name.orEmpty(),
description = type2.description,
emoji = type2.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type2.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
)
)
@ -1508,7 +1515,10 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
key = type1.uniqueKey.orEmpty(),
name = type1.name.orEmpty(),
description = type1.description,
emoji = type1.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type1.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
),
SlashItem.ObjectType(
@ -1517,7 +1527,10 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
key = type2.uniqueKey.orEmpty(),
name = type2.name.orEmpty(),
description = type2.description,
emoji = type2.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type2.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
),
SlashItem.ObjectType(
@ -1526,7 +1539,10 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
key = type3.uniqueKey.orEmpty(),
name = type3.name.orEmpty(),
description = type3.description,
emoji = type3.iconEmoji
icon = ObjectIcon.Basic.Emoji(
unicode = type3.iconEmoji.orEmpty(),
emptyState = ObjectIcon.Empty.ObjectType
),
)
)
)

View file

@ -15,6 +15,7 @@ import com.anytypeio.anytype.presentation.editor.EditorViewModel.Companion.TEXT_
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -96,7 +97,7 @@ class EditorSlashWidgetObjectTypeTest : EditorPresentationTestSetup() {
id = type2.id,
name = type2.name.orEmpty(),
description = type2.description,
emoji = type2.iconEmoji,
icon = ObjectIcon.None,
key = type2.getValue<Key>(Relations.UNIQUE_KEY)!!
)
)
@ -164,8 +165,8 @@ class EditorSlashWidgetObjectTypeTest : EditorPresentationTestSetup() {
id = type2.id,
name = type2.name.orEmpty(),
description = type2.description,
emoji = type2.iconEmoji,
key = type2.getValue(Relations.UNIQUE_KEY)!!
key = type2.getValue(Relations.UNIQUE_KEY)!!,
icon = ObjectIcon.None
)
)
)

View file

@ -23,6 +23,8 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.objects.ObjectTypeChangeViewModel
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
import com.anytypeio.anytype.core_models.SupportedLayouts
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.test_utils.MockDataFactory
@ -530,7 +532,7 @@ class ObjectTypeChangeViewModelTest {
key = marketplaceType3.uniqueKey.orEmpty(),
name = marketplaceType3.name.orEmpty(),
description = marketplaceType3.description.orEmpty(),
emoji = marketplaceType3.iconEmoji.orEmpty(),
icon = ObjectIcon.None
)
vm.onItemClicked(item)
assertEquals(
@ -594,12 +596,16 @@ class ObjectTypeChangeViewModelTest {
}
}
@Mock
lateinit var urlBuilder: UrlBuilder
private fun givenViewModel() = ObjectTypeChangeViewModel(
getObjectTypes = getObjectTypes,
addObjectTypeToSpace = addObjectToSpace,
dispatchers = dispatchers,
spaceManager = spaceManager,
getDefaultObjectType = getDefaultObjectType
getDefaultObjectType = getDefaultObjectType,
urlBuilder = urlBuilder
)
fun stubSpaceManager(spaceId: Id) {