diff --git a/sdk/compose/icons/api/icons.api b/sdk/compose/icons/api/icons.api index 7ed287bd..1640183f 100644 --- a/sdk/compose/icons/api/icons.api +++ b/sdk/compose/icons/api/icons.api @@ -53,6 +53,10 @@ public final class io/github/composegears/valkyrie/sdk/compose/icons/colored/Luc public static final fun getLucideLogo (Lio/github/composegears/valkyrie/sdk/compose/icons/ValkyrieIcons$Colored;)Landroidx/compose/ui/graphics/vector/ImageVector; } +public final class io/github/composegears/valkyrie/sdk/compose/icons/colored/OcticonsLogoKt { + public static final fun getOcticonsLogo (Lio/github/composegears/valkyrie/sdk/compose/icons/ValkyrieIcons$Colored;)Landroidx/compose/ui/graphics/vector/ImageVector; +} + public final class io/github/composegears/valkyrie/sdk/compose/icons/colored/PluginIconKt { public static final fun getPluginIcon (Lio/github/composegears/valkyrie/sdk/compose/icons/ValkyrieIcons$Colored;)Landroidx/compose/ui/graphics/vector/ImageVector; } diff --git a/sdk/compose/icons/api/icons.klib.api b/sdk/compose/icons/api/icons.klib.api index 6776d039..b20ce2c5 100644 --- a/sdk/compose/icons/api/icons.klib.api +++ b/sdk/compose/icons/api/icons.klib.api @@ -32,6 +32,8 @@ final val io.github.composegears.valkyrie.sdk.compose.icons.colored/IoniconsLogo final fun (io.github.composegears.valkyrie.sdk.compose.icons/ValkyrieIcons.Colored).(): androidx.compose.ui.graphics.vector/ImageVector // io.github.composegears.valkyrie.sdk.compose.icons.colored/IoniconsLogo.|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored(){}[0] final val io.github.composegears.valkyrie.sdk.compose.icons.colored/LucideLogo // io.github.composegears.valkyrie.sdk.compose.icons.colored/LucideLogo|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored{}LucideLogo[0] final fun (io.github.composegears.valkyrie.sdk.compose.icons/ValkyrieIcons.Colored).(): androidx.compose.ui.graphics.vector/ImageVector // io.github.composegears.valkyrie.sdk.compose.icons.colored/LucideLogo.|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored(){}[0] +final val io.github.composegears.valkyrie.sdk.compose.icons.colored/OcticonsLogo // io.github.composegears.valkyrie.sdk.compose.icons.colored/OcticonsLogo|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored{}OcticonsLogo[0] + final fun (io.github.composegears.valkyrie.sdk.compose.icons/ValkyrieIcons.Colored).(): androidx.compose.ui.graphics.vector/ImageVector // io.github.composegears.valkyrie.sdk.compose.icons.colored/OcticonsLogo.|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored(){}[0] final val io.github.composegears.valkyrie.sdk.compose.icons.colored/PluginIcon // io.github.composegears.valkyrie.sdk.compose.icons.colored/PluginIcon|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored{}PluginIcon[0] final fun (io.github.composegears.valkyrie.sdk.compose.icons/ValkyrieIcons.Colored).(): androidx.compose.ui.graphics.vector/ImageVector // io.github.composegears.valkyrie.sdk.compose.icons.colored/PluginIcon.|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored(){}[0] final val io.github.composegears.valkyrie.sdk.compose.icons.colored/RemixLogo // io.github.composegears.valkyrie.sdk.compose.icons.colored/RemixLogo|@io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons.Colored{}RemixLogo[0] diff --git a/sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/OcticonsLogo.kt b/sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/OcticonsLogo.kt new file mode 100644 index 00000000..bb69b6db --- /dev/null +++ b/sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/OcticonsLogo.kt @@ -0,0 +1,63 @@ +package io.github.composegears.valkyrie.sdk.compose.icons.colored + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.addPathNodes +import androidx.compose.ui.unit.dp +import io.github.composegears.valkyrie.sdk.compose.icons.ValkyrieIcons + +@Suppress("UnusedReceiverParameter") +val ValkyrieIcons.Colored.OcticonsLogo: ImageVector + get() { + if (_OcticonsLogo != null) { + return _OcticonsLogo!! + } + _OcticonsLogo = ImageVector.Builder( + name = "Colored.OcticonsLogo", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 460f, + viewportHeight = 460f, + ).apply { + addPath( + pathData = addPathNodes("M0,0h460v460h-460z"), + fill = SolidColor(Color(0xFF0969DA)), + ) + addPath( + pathData = addPathNodes( + "M75.642,258.999 C75.643,220.011 75.695,181.523 75.562,143.035 C75.549,139.19 76.613,137.748 80.511,136.756 C100.974,131.551 121.318,125.875 141.718,120.418 C170.788,112.643 199.867,104.896 228.965,97.223 C230.487,96.821 232.311,96.894 233.845,97.305 C266.606,106.084 299.332,114.996 332.095,123.764 C349.285,128.364 366.535,132.734 383.752,137.232 C384.706,137.481 385.603,137.948 386.844,138.44 C386.844,140.048 386.844,141.698 386.844,143.348 C386.844,202.33 386.804,261.312 386.935,320.293 C386.944,324.182 385.816,325.518 382.015,326.491 C362.029,331.608 342.156,337.164 322.229,342.513 C292.505,350.491 262.772,358.435 233.025,366.329 C231.826,366.647 230.383,366.553 229.167,366.227 C203.777,359.429 178.414,352.535 153.025,345.733 C128.438,339.146 103.856,332.536 79.198,326.224 C75.778,325.348 75.609,323.674 75.616,320.981 C75.667,300.487 75.643,279.993 75.642,258.999 Z", + ), + fill = SolidColor(Color(0xFFFAFAFA)), + ) + addPath( + pathData = addPathNodes( + "M184.128,332.842 C155.736,325.32 127.748,317.873 99.748,310.474 C97.479,309.875 96.04,309.246 96.048,306.29 C96.164,261.463 96.138,216.636 96.158,171.808 C96.159,171.323 96.34,170.838 96.523,169.92 C99.298,170.486 102.073,170.896 104.761,171.623 C138.18,180.66 171.585,189.75 205,198.806 C209.016,199.895 213.032,201.022 217.108,201.837 C220.164,202.448 221.009,204.009 220.983,207.028 C220.845,223.025 220.918,239.024 220.918,255.021 C220.918,282.184 220.918,309.347 220.918,336.51 C220.918,338.327 220.918,340.144 220.918,342.834 C208.43,339.434 196.48,336.181 184.128,332.842 Z", + ), + fill = SolidColor(Color(0xFF0969DA)), + ) + addPath( + pathData = addPathNodes( + "M283.047,214 C283.047,223.448 283.047,232.396 283.047,242.075 C297.361,238.3 310.962,234.714 324.715,231.088 C324.715,216.297 324.973,201.978 324.581,187.677 C324.435,182.327 325.75,179.79 331.295,178.582 C342.801,176.077 354.134,172.77 366.188,169.623 C366.307,171.899 366.465,173.499 366.462,175.099 C366.386,218.405 366.243,261.71 366.295,305.015 C366.299,308.57 365.135,309.9 361.846,310.765 C326.932,319.945 292.055,329.263 257.17,338.551 C252.211,339.872 247.265,341.24 241.453,342.82 C241.453,340.712 241.453,339.14 241.453,337.568 C241.453,294.427 241.501,251.286 241.37,208.146 C241.358,204.403 242.199,202.487 246.083,201.523 C258.163,198.523 270.149,195.146 283.047,191.68 C283.047,199.358 283.047,206.429 283.047,214 Z", + ), + fill = SolidColor(Color(0xFF0969DA)), + ) + addPath( + pathData = addPathNodes( + "M203.532,149.449 C229.361,156.428 254.8,163.288 280.24,170.147 C280.245,170.453 280.25,170.76 280.255,171.066 C275.832,172.399 271.439,173.846 266.98,175.042 C255.901,178.015 244.807,180.934 233.679,183.714 C232.015,184.13 230.043,184.014 228.364,183.572 C199.907,176.089 171.474,168.51 143.033,160.963 C130.342,157.595 117.639,154.277 104.959,150.87 C103.213,150.401 101.57,149.547 100.009,147.759 C115.717,143.609 131.358,139.164 147.185,135.534 C150.73,134.721 154.988,136.531 158.816,137.526 C173.614,141.374 188.37,145.381 203.532,149.449 Z", + ), + fill = SolidColor(Color(0xFF0969DA)), + ) + addPath( + pathData = addPathNodes( + "M269.889,123.193 C300.967,131.456 331.647,139.623 362.328,147.791 C362.328,148.343 362.328,148.895 362.328,149.447 C358.505,150.572 354.687,151.713 350.856,152.811 C349.577,153.178 348.157,153.212 346.985,153.778 C326.594,163.617 307.466,155.039 288.086,149.847 C256.918,141.497 225.73,133.216 194.557,124.885 C193.626,124.636 192.759,124.153 191.035,123.435 C204.206,119.972 216.516,116.572 228.929,113.606 C231.507,112.99 234.567,113.606 237.238,114.299 C248.025,117.095 258.746,120.143 269.889,123.193 Z", + ), + fill = SolidColor(Color(0xFF0969DA)), + ) + }.build() + + return _OcticonsLogo!! + } + +@Suppress("ObjectPropertyName") +private var _OcticonsLogo: ImageVector? = null diff --git a/tools/idea-plugin/CHANGELOG.md b/tools/idea-plugin/CHANGELOG.md index edf24b32..b55f5545 100644 --- a/tools/idea-plugin/CHANGELOG.md +++ b/tools/idea-plugin/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - [Web Import] Add `css.gg` icons provider +- [Web Import] Add `Octicons` icons provider - [Web Import] Add `Simple` icons provider - [Web Import] Add `Hero` icons provider - [Web Import] Add floating zoom control bar to the icon grid — allows changing the visual display size of icons ( diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/service/PersistentSettings.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/service/PersistentSettings.kt index f734c2d4..9f2c2bc8 100644 --- a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/service/PersistentSettings.kt +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/service/PersistentSettings.kt @@ -98,6 +98,9 @@ class PersistentSettings : SimplePersistentStateComponent(Valkyri // css.gg var cssGgSize: Int by property(DEFAULT_SIZE) + + // Octicons + var octiconsSize: Int by property(DEFAULT_SIZE) } companion object { diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/settings/InMemorySettings.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/settings/InMemorySettings.kt index 835b3f6d..c8b710f3 100644 --- a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/settings/InMemorySettings.kt +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/settings/InMemorySettings.kt @@ -78,6 +78,7 @@ class InMemorySettings(project: Project) { simpleIconsSize = DEFAULT_SIZE heroiconsSize = DEFAULT_SIZE cssGgSize = DEFAULT_SIZE + octiconsSize = DEFAULT_SIZE } fun updateUIState(uiState: SavedState) { diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt index e64054d9..86a7cec1 100644 --- a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt @@ -19,6 +19,7 @@ import io.github.composegears.valkyrie.ui.screen.webimport.standard.simpleicons. import io.github.composegears.valkyrie.ui.screen.webimport.standard.tabler.TablerImportScreen import io.github.composegears.valkyrie.ui.screen.webimport.svg.cssgg.CssGgImportScreen import io.github.composegears.valkyrie.ui.screen.webimport.svg.heroicons.HeroiconsImportScreen +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.OcticonsImportScreen val WebImportFlow by navDestination { Navigation( @@ -38,6 +39,7 @@ val WebImportFlow by navDestination { EvaImportScreen, FeatherImportScreen, HeroiconsImportScreen, + OcticonsImportScreen, IoniconsImportScreen, SimpleIconsImportScreen, CssGgImportScreen, diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt index 6a13efb1..d83131ed 100644 --- a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt @@ -32,6 +32,7 @@ import io.github.composegears.valkyrie.sdk.compose.icons.colored.GoogleMaterialL import io.github.composegears.valkyrie.sdk.compose.icons.colored.HeroiconsLogo import io.github.composegears.valkyrie.sdk.compose.icons.colored.IoniconsLogo import io.github.composegears.valkyrie.sdk.compose.icons.colored.LucideLogo +import io.github.composegears.valkyrie.sdk.compose.icons.colored.OcticonsLogo import io.github.composegears.valkyrie.sdk.compose.icons.colored.RemixLogo import io.github.composegears.valkyrie.sdk.compose.icons.colored.SimpleLogo import io.github.composegears.valkyrie.sdk.compose.icons.colored.TablerLogo @@ -45,6 +46,7 @@ import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.GoogleM import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Heroicons import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Ionicons import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Lucide +import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Octicons import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Remix import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.SimpleIcons import io.github.composegears.valkyrie.ui.screen.webimport.IconProviders.Tabler @@ -61,6 +63,7 @@ import io.github.composegears.valkyrie.ui.screen.webimport.standard.simpleicons. import io.github.composegears.valkyrie.ui.screen.webimport.standard.tabler.TablerImportScreen import io.github.composegears.valkyrie.ui.screen.webimport.svg.cssgg.CssGgImportScreen import io.github.composegears.valkyrie.ui.screen.webimport.svg.heroicons.HeroiconsImportScreen +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.OcticonsImportScreen import io.github.composegears.valkyrie.util.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer @@ -82,6 +85,7 @@ val WebImportSelectorScreen by navDestination { Eva -> EvaImportScreen Feather -> FeatherImportScreen Heroicons -> HeroiconsImportScreen + Octicons -> OcticonsImportScreen Ionicons -> IoniconsImportScreen SimpleIcons -> SimpleIconsImportScreen CssGg -> CssGgImportScreen @@ -180,6 +184,11 @@ enum class IconProviders( titleKey = "web.import.selector.lucide.title", descriptionKey = "web.import.selector.lucide.description", ), + Octicons( + icon = ValkyrieIcons.Colored.OcticonsLogo, + titleKey = "web.import.selector.octicons.title", + descriptionKey = "web.import.selector.octicons.description", + ), Remix( icon = ValkyrieIcons.Colored.RemixLogo, titleKey = "web.import.selector.remix.title", diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/OcticonsImportScreen.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/OcticonsImportScreen.kt new file mode 100644 index 00000000..bc5065d5 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/OcticonsImportScreen.kt @@ -0,0 +1,40 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons + +import androidx.compose.runtime.Composable +import com.composegears.leviathan.compose.inject +import com.composegears.tiamat.compose.TiamatPreview +import com.composegears.tiamat.compose.back +import com.composegears.tiamat.compose.navController +import com.composegears.tiamat.compose.navDestination +import com.composegears.tiamat.compose.navigate +import io.github.composegears.valkyrie.jewel.tooling.ProjectPreviewTheme +import io.github.composegears.valkyrie.ui.screen.mode.simple.conversion.SimpleConversionParamsSource.TextSource +import io.github.composegears.valkyrie.ui.screen.mode.simple.conversion.SimpleConversionScreen +import io.github.composegears.valkyrie.ui.screen.webimport.svg.common.SvgImportScreen +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.di.OcticonsModule +import io.github.composegears.valkyrie.util.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +val OcticonsImportScreen by navDestination { + val navController = navController() + SvgImportScreen( + title = stringResource("web.import.title.octicons"), + provider = inject(OcticonsModule.octiconsUseCase), + onBack = navController::back, + onIconDownload = { + navController.parent?.navigate( + dest = SimpleConversionScreen, + navArgs = TextSource( + name = it.name, + text = it.svgContent, + ), + ) + }, + ) +} + +@Preview +@Composable +private fun OcticonsImportScreenPreview() = ProjectPreviewTheme { + TiamatPreview(OcticonsImportScreen) +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsIconMetadata.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsIconMetadata.kt new file mode 100644 index 00000000..7320f2e3 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsIconMetadata.kt @@ -0,0 +1,9 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +data class OcticonsIconMetadata( + val name: String, + val size: String, + val width: Int, + val path: String, + val tags: List, +) diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParser.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParser.kt new file mode 100644 index 00000000..2a038193 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParser.kt @@ -0,0 +1,40 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class OcticonsMetadataParser( + private val json: Json = Json, +) { + fun parse(text: String): List { + val root = json.parseToJsonElement(text).jsonObject + return root.flatMap { (name, iconElement) -> + val iconObject = iconElement.jsonObject + val keywords = iconObject["keywords"] + ?.jsonArray + ?.mapNotNull { it.jsonPrimitive.contentOrNull } + .orEmpty() + val tags = (name.split('-') + keywords).distinct() + + iconObject["heights"] + ?.jsonObject + ?.entries + .orEmpty() + .sortedBy { it.key.toIntOrNull() ?: Int.MAX_VALUE } + .mapNotNull { (size, heightElement) -> + val width = heightElement.jsonObject["width"]?.jsonPrimitive?.content?.toIntOrNull() + ?: return@mapNotNull null + OcticonsIconMetadata( + name = name, + size = size, + width = width, + path = "$name-$size.svg", + tags = tags, + ) + } + } + } +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsRepository.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsRepository.kt new file mode 100644 index 00000000..b7e52391 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsRepository.kt @@ -0,0 +1,52 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +import io.github.composegears.valkyrie.util.coroutines.suspendLazy +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class OcticonsRepository( + private val httpClient: HttpClient, + private val json: Json, + private val metadataParser: OcticonsMetadataParser, +) { + private val latestVersion = suspendLazy { + withContext(Dispatchers.IO) { + json.parseToJsonElement(httpClient.get(PACKAGE_JSON_URL).bodyAsText()) + .jsonObject["version"] + ?.jsonPrimitive + ?.content + ?: error("Failed to resolve Octicons package version") + } + } + + private val metadata = suspendLazy { + withContext(Dispatchers.IO) { + metadataParser.parse(httpClient.get(resolveMetadataUrl(latestVersion())).bodyAsText()) + } + } + + suspend fun loadMetadata(): List = metadata() + + suspend fun downloadSvg(path: String): String = withContext(Dispatchers.IO) { + val response = httpClient.get(resolveOcticonsSvgUrl(path, version = latestVersion())) + if (!response.status.isSuccess()) { + error("Failed to download css.gg SVG: HTTP ${response.status.value}") + } + response.bodyAsText() + } + + private companion object { + private const val PACKAGE_JSON_URL = "https://cdn.jsdelivr.net/npm/@primer/octicons@latest/package.json" + + private fun resolveMetadataUrl(version: String): String { + return "https://cdn.jsdelivr.net/npm/@primer/octicons@$version/build/data.json" + } + } +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolver.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolver.kt new file mode 100644 index 00000000..b117ebec --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolver.kt @@ -0,0 +1,5 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +fun resolveOcticonsSvgUrl(path: String, version: String): String { + return "https://cdn.jsdelivr.net/npm/@primer/octicons@$version/build/svg/${path.trimStart('/')}" +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/di/OcticonsModule.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/di/OcticonsModule.kt new file mode 100644 index 00000000..c1a93c84 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/di/OcticonsModule.kt @@ -0,0 +1,30 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.di + +import com.composegears.leviathan.Leviathan +import io.github.composegears.valkyrie.ui.di.coreModule +import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data.OcticonsMetadataParser +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data.OcticonsRepository +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.domain.OcticonsUseCase + +object OcticonsModule : Leviathan() { + private val network = NetworkModule + private val core = coreModule() + + private val octiconsMetadataParser by factoryOf { OcticonsMetadataParser(json = inject(network.json)) } + + private val octiconsRepository by instanceOf { + OcticonsRepository( + httpClient = inject(network.httpClient), + json = inject(network.json), + metadataParser = inject(octiconsMetadataParser), + ) + } + + val octiconsUseCase by instanceOf { + OcticonsUseCase( + repository = inject(octiconsRepository), + inMemorySettings = inject(core.inMemorySettings), + ) + } +} diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilder.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilder.kt new file mode 100644 index 00000000..e448d836 --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilder.kt @@ -0,0 +1,39 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.domain + +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.IconStyle +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.inferCategoryFromTags +import io.github.composegears.valkyrie.ui.screen.webimport.common.util.toDisplayName +import io.github.composegears.valkyrie.ui.screen.webimport.svg.common.model.SvgIcon +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data.OcticonsIconMetadata + +fun buildOcticons(metadata: List): List { + val supportedMetadata = metadata.filter { it.size in SUPPORTED_OCTICONS_SIZES && !it.name.hasRestrictedBrandMark() } + val namesWithVariants = supportedMetadata + .groupingBy { it.name } + .eachCount() + .filterValues { it > 1 } + .keys + + return supportedMetadata.map { icon -> + val style = icon.size.toOcticonsStyle() + SvgIcon( + name = icon.name, + displayName = icon.name.toDisplayName(), + exportName = if (icon.name in namesWithVariants) "${icon.name}-${style.id}" else icon.name, + path = icon.path, + tags = icon.tags, + category = inferCategoryFromTags(icon.name, icon.tags), + style = style, + ) + } +} + +private fun String.toOcticonsStyle(): IconStyle { + return IconStyle(id = this, name = "${this}px") +} + +private fun String.hasRestrictedBrandMark(): Boolean { + return startsWith("logo-") || startsWith("lockup-") || startsWith("mark-") +} + +private val SUPPORTED_OCTICONS_SIZES = setOf("16", "24") diff --git a/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsUseCase.kt b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsUseCase.kt new file mode 100644 index 00000000..847cb42a --- /dev/null +++ b/tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsUseCase.kt @@ -0,0 +1,40 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.domain + +import io.github.composegears.valkyrie.settings.InMemorySettings +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.IconStyle +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.WebIconConfig +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.toWebIconConfig +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.settings.SizeSettings +import io.github.composegears.valkyrie.ui.screen.webimport.common.util.SvgSizeCustomizer +import io.github.composegears.valkyrie.ui.screen.webimport.svg.common.domain.SvgIconProvider +import io.github.composegears.valkyrie.ui.screen.webimport.svg.common.model.SvgIcon +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data.OcticonsRepository + +class OcticonsUseCase( + private val repository: OcticonsRepository, + private val inMemorySettings: InMemorySettings, +) : SvgIconProvider { + + override val providerName: String = "Octicons" + override val stateKey: String = "octicons" + override val persistentSize: Int = inMemorySettings.readState { octiconsSize } + + override fun updatePersistentSize(value: Int) { + inMemorySettings.update { + octiconsSize = value + } + } + + override suspend fun loadConfig(): WebIconConfig { + return buildOcticons(repository.loadMetadata()).toWebIconConfig() + } + + override suspend fun loadPreviewSvg(icon: SvgIcon): String { + return repository.downloadSvg(icon.path) + } + + override suspend fun downloadSvg(icon: SvgIcon, settings: SizeSettings, style: IconStyle?): String { + val rawSvg = repository.downloadSvg(icon.path) + return SvgSizeCustomizer.applySettings(rawSvg, settings) + } +} diff --git a/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties b/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties index 17c5441e..e519cd22 100644 --- a/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties +++ b/tools/idea-plugin/src/main/resources/messages/Valkyrie.properties @@ -141,6 +141,8 @@ web.import.selector.feather.title=Feather Icons web.import.selector.feather.description=Simply beautiful open-source icons with a clean outline style web.import.selector.heroicons.title=Heroicons web.import.selector.heroicons.description=Beautiful hand-crafted SVG icons by the makers of Tailwind CSS +web.import.selector.octicons.title=Octicons +web.import.selector.octicons.description=GitHub's SVG icon set for interfaces, repo actions, and developer tools web.import.selector.ionicons.title=Ionicons web.import.selector.ionicons.description=Premium designed icons for use in web, iOS, Android, and desktop apps web.import.selector.simpleicons.title=Simple Icons @@ -168,6 +170,7 @@ web.import.title.tabler=Tabler Icons import web.import.title.eva=Eva Icons import web.import.title.feather=Feather Icons import web.import.title.heroicons=Heroicons import +web.import.title.octicons=Octicons import web.import.title.ionicons=Ionicons import web.import.title.simpleicons=Simple Icons import imagevectortoxml.picker.title=ImageVector to XML diff --git a/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParserTest.kt b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParserTest.kt new file mode 100644 index 00000000..5c7a201c --- /dev/null +++ b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsMetadataParserTest.kt @@ -0,0 +1,42 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEqualTo +import org.junit.jupiter.api.Test + +class OcticonsMetadataParserTest { + + @Test + fun `parse octicons metadata entries from package data`() { + val parser = OcticonsMetadataParser() + val metadataJson = """ + { + "alert": { + "name": "alert", + "keywords": ["warning", "danger"], + "heights": { + "16": { "width": 16 }, + "24": { "width": 24 } + } + }, + "arrow-left": { + "name": "arrow-left", + "keywords": [], + "heights": { + "24": { "width": 24 } + } + } + } + """.trimIndent() + + val icons = parser.parse(metadataJson) + + assertThat(icons.map { it.name }).containsExactly("alert", "alert", "arrow-left") + assertThat(icons.map { it.size }).containsExactly("16", "24", "24") + assertThat(icons.map { it.width }).containsExactly(16, 24, 24) + assertThat(icons.map { it.path }).containsExactly("alert-16.svg", "alert-24.svg", "arrow-left-24.svg") + assertThat(icons[0].tags).isEqualTo(listOf("alert", "warning", "danger")) + assertThat(icons[2].tags).isEqualTo(listOf("arrow", "left")) + } +} diff --git a/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolverTest.kt b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolverTest.kt new file mode 100644 index 00000000..bc073517 --- /dev/null +++ b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/data/OcticonsSvgPathResolverTest.kt @@ -0,0 +1,15 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data + +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.jupiter.api.Test + +class OcticonsSvgPathResolverTest { + + @Test + fun `resolve octicons svg url`() { + assertThat(resolveOcticonsSvgUrl("alert-16.svg", version = "19.16.0")).isEqualTo( + "https://cdn.jsdelivr.net/npm/@primer/octicons@19.16.0/build/svg/alert-16.svg", + ) + } +} diff --git a/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilderTest.kt b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilderTest.kt new file mode 100644 index 00000000..80b4dc99 --- /dev/null +++ b/tools/idea-plugin/src/test/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/svg/octicons/domain/OcticonsConfigBuilderTest.kt @@ -0,0 +1,79 @@ +package io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.domain + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.hasSize +import assertk.assertions.isEqualTo +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.category.InferredCategory +import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.IconStyle +import io.github.composegears.valkyrie.ui.screen.webimport.svg.octicons.data.OcticonsIconMetadata +import org.junit.jupiter.api.Test + +class OcticonsConfigBuilderTest { + + @Test + fun `build octicons with size styles`() { + val icons = buildOcticons( + metadata = listOf( + OcticonsIconMetadata( + name = "alert", + size = "16", + width = 16, + path = "alert-16.svg", + tags = listOf("alert", "warning"), + ), + OcticonsIconMetadata( + name = "alert", + size = "24", + width = 24, + path = "alert-24.svg", + tags = listOf("alert", "warning"), + ), + OcticonsIconMetadata( + name = "alert", + size = "12", + width = 12, + path = "alert-12.svg", + tags = listOf("alert", "warning"), + ), + OcticonsIconMetadata( + name = "feed-issue-reopen", + size = "16", + width = 17, + path = "feed-issue-reopen-16.svg", + tags = listOf("feed", "issue", "reopen"), + ), + OcticonsIconMetadata( + name = "lockup-github", + size = "16", + width = 68, + path = "lockup-github-16.svg", + tags = listOf("lockup", "github"), + ), + OcticonsIconMetadata( + name = "archive", + size = "16", + width = 16, + path = "archive-16.svg", + tags = listOf("archive"), + ), + ), + ) + + assertThat(icons).hasSize(4) + assertThat(icons.map { it.name }).containsExactly("alert", "alert", "feed-issue-reopen", "archive") + assertThat(icons.map { it.displayName }).containsExactly("Alert", "Alert", "Feed Issue Reopen", "Archive") + assertThat(icons.map { it.path }).containsExactly( + "alert-16.svg", + "alert-24.svg", + "feed-issue-reopen-16.svg", + "archive-16.svg", + ) + assertThat(icons.map { it.exportName }).containsExactly("alert-16", "alert-24", "feed-issue-reopen", "archive") + assertThat(icons.map { it.style }).containsExactly(SIZE_16_STYLE, SIZE_24_STYLE, SIZE_16_STYLE, SIZE_16_STYLE) + assertThat(icons[0].category).isEqualTo(InferredCategory(id = "general", name = "General")) + } +} + +private val SIZE_16_STYLE = IconStyle(id = "16", name = "16px") +private val SIZE_24_STYLE = IconStyle(id = "24", name = "24px")