From 78b75860d5a5a456dec9009bfd945a5d8df07895 Mon Sep 17 00:00:00 2001 From: Yahor Urbanovich Date: Fri, 29 May 2026 20:11:37 +0300 Subject: [PATCH] Add contributors section to About screen --- .../tabs/about/AboutSettingsScreen.kt | 39 ++++++++- .../tabs/about/AboutSettingsViewModel.kt | 81 +++++++++++++++++++ .../tabs/about/domain/AvatarByteArray.kt | 7 ++ .../tabs/about/domain/ContributorUiModel.kt | 9 +++ .../settings/tabs/about/ui/ContributorItem.kt | 73 +++++++++++++++++ .../util/result/RunCatchingCancelable.kt | 13 +++ .../resources/messages/Valkyrie.properties | 1 + 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsViewModel.kt create mode 100644 tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/AvatarByteArray.kt create mode 100644 tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/ContributorUiModel.kt create mode 100644 tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/ui/ContributorItem.kt create mode 100644 tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/result/RunCatchingCancelable.kt diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsScreen.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsScreen.kt index 19632c12a..6aa957469 100644 --- a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsScreen.kt +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsScreen.kt @@ -3,20 +3,30 @@ package io.github.composegears.valkyrie.ui.screen.settings.tabs.about import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.composegears.tiamat.compose.TiamatPreview import com.composegears.tiamat.compose.navDestination import io.github.composegears.valkyrie.ValkyrieBuildConfig import io.github.composegears.valkyrie.jewel.HorizontalDivider import io.github.composegears.valkyrie.jewel.settings.GroupSpacing import io.github.composegears.valkyrie.jewel.tooling.PreviewTheme +import io.github.composegears.valkyrie.sdk.compose.foundation.animation.rememberShimmer import io.github.composegears.valkyrie.sdk.compose.foundation.layout.CenterVerticalRow import io.github.composegears.valkyrie.sdk.compose.foundation.layout.Spacer import io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons import io.github.composegears.valkyrie.sdk.compose.icons.colored.PluginIcon +import io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain.ContributorUiModel +import io.github.composegears.valkyrie.ui.screen.settings.tabs.about.ui.ContributorItem import io.github.composegears.valkyrie.util.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.jewel.foundation.theme.JewelTheme @@ -27,11 +37,16 @@ import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer import org.jetbrains.jewel.ui.typography val AboutSettingsScreen by navDestination { - AboutSettingsUi() + val viewModel = viewModel { AboutSettingsViewModel() } + val contributors by viewModel.contributors.collectAsState() + + AboutSettingsUi(contributors = contributors) } @Composable -private fun AboutSettingsUi() { +private fun AboutSettingsUi(contributors: List) { + val shimmer = rememberShimmer() + VerticallyScrollableContainer { Column(modifier = Modifier.padding(horizontal = 16.dp)) { CenterVerticalRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { @@ -68,6 +83,24 @@ private fun AboutSettingsUi() { uri = "https://plugins.jetbrains.com/plugin/24786-valkyrie", text = stringResource("settings.rate.marketplace"), ) + Spacer(16.dp) + HorizontalDivider() + Spacer(16.dp) + Text( + text = stringResource("settings.about.contributors"), + style = JewelTheme.typography.h4TextStyle, + maxLines = 1, + ) + Spacer(12.dp) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + contributors.forEach { contributor -> + ContributorItem(contributor = contributor, shimmer = shimmer) + } + } GroupSpacing() } } @@ -76,5 +109,5 @@ private fun AboutSettingsUi() { @Preview @Composable private fun AboutSettingsPreview() = PreviewTheme { - AboutSettingsUi() + TiamatPreview(AboutSettingsScreen) } diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsViewModel.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsViewModel.kt new file mode 100644 index 000000000..3a75e8d70 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/AboutSettingsViewModel.kt @@ -0,0 +1,81 @@ +package io.github.composegears.valkyrie.ui.screen.settings.tabs.about + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.composegears.leviathan.compose.inject +import io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain.AvatarByteArray +import io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain.ContributorUiModel +import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule +import io.github.composegears.valkyrie.util.result.runCatchingCancellable +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsBytes +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class AboutSettingsViewModel : ViewModel() { + + private val httpClient = inject(NetworkModule.httpClient) + + private val _contributors = MutableStateFlow(initialContributors()) + val contributors = _contributors.asStateFlow() + + init { + loadAvatars() + } + + private fun loadAvatars() { + viewModelScope.launch { + val updated = _contributors.value.map { contributor -> + async { + val bytes = runCatchingCancellable { + AvatarByteArray(httpClient.get(contributor.avatarUrl).bodyAsBytes()) + }.getOrNull() + contributor.copy(avatarBytes = bytes) + } + }.awaitAll() + _contributors.value = updated + } + } + + private fun initialContributors(): List = listOf( + ContributorUiModel( + login = "egorikftp", + description = "Project creator & core maintainer", + profileUrl = "https://github.com/egorikftp", + avatarUrl = "https://github.com/egorikftp.png", + ), + ContributorUiModel( + login = "t-regbs", + description = "Web import & Figma plugin development, ImageVector to XML conversion, KMP tuning", + profileUrl = "https://github.com/t-regbs", + avatarUrl = "https://github.com/t-regbs.png", + ), + ContributorUiModel( + login = "Goooler", + description = "Build tooling, test infrastructure & dependency management", + profileUrl = "https://github.com/Goooler", + avatarUrl = "https://github.com/Goooler.png", + ), + ContributorUiModel( + login = "LennartEgb", + description = "KMP generator & parser modules, IR restructuring & Compose colors handling", + profileUrl = "https://github.com/LennartEgb", + avatarUrl = "https://github.com/LennartEgb.png", + ), + ContributorUiModel( + login = "vkatz", + description = "Fuzzy search, plugin state persistence & technical support", + profileUrl = "https://github.com/vkatz", + avatarUrl = "https://github.com/vkatz.png", + ), + ContributorUiModel( + login = "jonapoul", + description = "Initial Gradle plugin implementation", + profileUrl = "https://github.com/jonapoul", + avatarUrl = "https://github.com/jonapoul.png", + ), + ) +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/AvatarByteArray.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/AvatarByteArray.kt new file mode 100644 index 000000000..46c02da38 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/AvatarByteArray.kt @@ -0,0 +1,7 @@ +package io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain + +import androidx.compose.runtime.Stable + +@Stable +@JvmInline +value class AvatarByteArray(val bytes: ByteArray) diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/ContributorUiModel.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/ContributorUiModel.kt new file mode 100644 index 000000000..4330aa00a --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/domain/ContributorUiModel.kt @@ -0,0 +1,9 @@ +package io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain + +data class ContributorUiModel( + val login: String, + val description: String, + val profileUrl: String, + val avatarUrl: String, + val avatarBytes: AvatarByteArray? = null, +) diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/ui/ContributorItem.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/ui/ContributorItem.kt new file mode 100644 index 000000000..17ac9e859 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/settings/tabs/about/ui/ContributorItem.kt @@ -0,0 +1,73 @@ +package io.github.composegears.valkyrie.ui.screen.settings.tabs.about.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.composegears.valkyrie.sdk.compose.foundation.animation.Shimmer +import io.github.composegears.valkyrie.sdk.compose.foundation.animation.shimmer +import io.github.composegears.valkyrie.ui.screen.settings.tabs.about.domain.ContributorUiModel +import java.io.ByteArrayInputStream +import javax.imageio.ImageIO +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.ExternalLink +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.typography + +@Composable +fun ContributorItem( + contributor: ContributorUiModel, + shimmer: Shimmer, + modifier: Modifier = Modifier, +) { + val bitmap: ImageBitmap? = remember(contributor.avatarBytes) { + contributor.avatarBytes?.let { avatar -> + runCatching { + ImageIO.read(ByteArrayInputStream(avatar.bytes)).toComposeImageBitmap() + }.getOrNull() + } + } + + Column( + modifier = modifier.width(160.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (bitmap != null) { + Image( + modifier = Modifier.size(40.dp).clip(CircleShape), + painter = BitmapPainter(bitmap), + contentDescription = contributor.login, + ) + } else { + Spacer( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .shimmer(shimmer = shimmer, cornerRadius = 20.dp), + ) + } + ExternalLink( + uri = contributor.profileUrl, + text = contributor.login, + ) + Text( + text = contributor.description, + style = JewelTheme.typography.small, + textAlign = TextAlign.Center, + ) + } +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/result/RunCatchingCancelable.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/result/RunCatchingCancelable.kt new file mode 100644 index 000000000..e4ad470dc --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/result/RunCatchingCancelable.kt @@ -0,0 +1,13 @@ +package io.github.composegears.valkyrie.util.result + +import kotlin.coroutines.cancellation.CancellationException + +inline fun runCatchingCancellable(block: () -> R): Result { + return try { + Result.success(block()) + } catch (c: CancellationException) { + throw c + } catch (e: Throwable) { + Result.failure(e) + } +} diff --git a/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties b/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties index 95064c87c..2a2def404 100644 --- a/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties +++ b/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties @@ -124,6 +124,7 @@ settings.about.plugin.name=Valkyrie Plugin settings.about.sourcecode=Source code settings.about.issue=Submit issue or idea settings.rate.marketplace=Rate on JetBrains Marketplace +settings.about.contributors=People behind the project ❤️ web.import.selector.title=Choose icon provider web.import.selector.google.title=Google Material Symbols web.import.selector.google.description=A collection of over 2,500 icons with various design options, available in a single font file