From 160f1f318f281104d0beb1e40ae7eda8036149d5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 25 Jul 2026 22:06:13 +0600 Subject: [PATCH] feat(plugin_discovery): implement GitHub plugin discovery and add logo support and information dialog in UI --- .../spotube/modules/plugin/BuiltInPlugins.kt | 12 +- .../modules/plugin/PluginDiscoverViewModel.kt | 8 +- .../spotube/modules/plugin/PluginManager.kt | 9 + .../spotube/modules/plugin/PluginModels.kt | 6 +- .../spotube/modules/plugin/PluginScreen.kt | 270 ++++++++++++++++-- .../modules/plugin/components/PluginCard.kt | 64 +++-- .../resources/iconsax/IconsaxInformation.kt | 97 +++++++ 7 files changed, 421 insertions(+), 45 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxInformation.kt diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/BuiltInPlugins.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/BuiltInPlugins.kt index 6ab97e80..9a642cd6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/BuiltInPlugins.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/BuiltInPlugins.kt @@ -27,7 +27,11 @@ val NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN = PluginEntry( PluginCapability.NETWORK_REQUESTS, PluginCapability.PERSISTENT_STORAGE ), - abilities = listOf(PluginAbility.AUDIO) + abilities = listOf(PluginAbility.AUDIO), + contact = "", + repository = "", + bugs = "", + license = "", ) val LRCLIB_BUILT_IN_PLUGIN = PluginEntry( name = "LRCLib Lyrics", @@ -38,7 +42,11 @@ val LRCLIB_BUILT_IN_PLUGIN = PluginEntry( capabilities = listOf( PluginCapability.NETWORK_REQUESTS, ), - abilities = listOf(PluginAbility.LYRICS) + abilities = listOf(PluginAbility.LYRICS), + contact = "", + repository = "", + bugs = "", + license = "", ) val BUILT_IN_PLUGINS = listOf( NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginDiscoverViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginDiscoverViewModel.kt index 1793a5df..bb79edb0 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginDiscoverViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginDiscoverViewModel.kt @@ -61,9 +61,9 @@ class PluginDiscoverViewModel( viewModelScope.launch { pluginManager.state.collect { pluginState -> if (pluginState is PluginManagerStates.Data) { - val installedAuthors = pluginState.plugins.map { it.author }.toSet() + val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet() _state.update { - it.copy(repos = _allRepos.filter { repo -> repo.owner.login !in installedAuthors }) + it.copy(repos = _allRepos.filter { repo -> repo.htmlUrl !in installedUrls }) } } } @@ -74,8 +74,8 @@ class PluginDiscoverViewModel( private fun filterInstalled(repos: List): List { val pluginState = pluginManager.state.value if (pluginState !is PluginManagerStates.Data) return repos - val installedAuthors = pluginState.plugins.map { it.author }.toSet() - return repos.filter { it.owner.login !in installedAuthors } + val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet() + return repos.filter { it.htmlUrl !in installedUrls } } private fun loadFirstPage() { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt index 19d701a5..0fe5276e 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt @@ -325,6 +325,15 @@ class PluginManager( throw IllegalArgumentException("Invalid plugin.json format: ${e.message}") } + // Preserve logo.png before deleting temp dir so the permission dialog can show it + val logoPngPath = tempDir / "logo.png".toPath() + if (FileSystem.SYSTEM.exists(logoPngPath)) { + val logoDir = pluginsDirPath / pluginEntry.id.toPath() + if (!FileSystem.SYSTEM.exists(logoDir)) FileSystem.SYSTEM.createDirectories(logoDir) + val destLogo = logoDir / "logo.png".toPath() + FileSystem.SYSTEM.copy(logoPngPath, destLogo) + } + pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes) } catch (e: Exception) { throw Exception("Failed to read plugin: ${e.message}", e) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginModels.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginModels.kt index 406ab554..1d7189e4 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginModels.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginModels.kt @@ -51,7 +51,11 @@ data class PluginEntry( val description: String, val author: String, val capabilities: List, - val abilities: List + val abilities: List, + val contact: String, + val repository: String, + val bugs: String, + val license: String, ) { @Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") val id: String = MurmurHash3().hash32x86("$name:$author".encodeToByteArray()) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt index d3d7f941..50d6a506 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt @@ -17,6 +17,7 @@ package dev.krtirtho.spotube.modules.plugin +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,6 +31,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -41,6 +44,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -50,16 +54,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import coil3.request.crossfade import dev.krtirtho.spotube.PlatformType +import dev.krtirtho.spotube.core.extras.kebabToTitleCase import dev.krtirtho.spotube.core.ui.base.Card import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton +import dev.krtirtho.spotube.core.ui.base.SecondaryButton import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton import dev.krtirtho.spotube.core.ui.base.TextField +import dev.krtirtho.spotube.core.ui.base.ThemedDialog import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem @@ -67,21 +79,21 @@ import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode import dev.krtirtho.spotube.core.webview.WebViewController import dev.krtirtho.spotube.getPlatform -import dev.krtirtho.spotube.openUrlInBrowser import dev.krtirtho.spotube.modules.plugin.components.PluginCard import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset +import dev.krtirtho.spotube.openUrlInBrowser +import dev.krtirtho.spotube.resources.iconsax.CarbonGithubLogo import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import dev.krtirtho.spotube.resources.iconsax.IconsaxBox import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle -import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentDownload import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText -import dev.krtirtho.spotube.resources.iconsax.IconsaxGlobe -import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk +import dev.krtirtho.spotube.resources.iconsax.IconsaxGlobe +import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk import dev.krtirtho.spotube.resources.iconsax.IconsaxLink import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic @@ -92,20 +104,9 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher import io.github.vinceglb.filekit.readBytes import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import net.swiftzer.semver.SemVer import okio.FileSystem import okio.Path.Companion.toPath -import androidx.compose.foundation.clickable -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.ui.platform.LocalDensity -import coil3.compose.AsyncImage -import coil3.compose.LocalPlatformContext -import coil3.request.ImageRequest -import coil3.request.crossfade -import dev.krtirtho.spotube.core.extras.kebabToTitleCase -import dev.krtirtho.spotube.core.ui.base.SecondaryButton -import dev.krtirtho.spotube.resources.iconsax.CarbonGithubLogo import okio.SYSTEM import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject @@ -164,6 +165,11 @@ fun PluginScreen( val discoverViewModel: PluginDiscoverViewModel = koinViewModel() val discoverState by discoverViewModel.state.collectAsStateWithLifecycle() + var showPluginInfo by remember { mutableStateOf(null) } + var showPluginSupport by remember { mutableStateOf(null) } + var supportText by remember { mutableStateOf(null) } + var isLoadingSupport by remember { mutableStateOf(false) } + val pleaseEnterUrl = stringResource(Res.string.plugin_error_enter_url) val urlSchemeError = stringResource(Res.string.plugin_error_url_scheme) val downloadFailed = stringResource(Res.string.plugin_error_download_failed) @@ -203,9 +209,8 @@ fun PluginScreen( } pendingPlugin?.let { pending -> - val logoPath = remember(pending.existingEntry?.id) { - val existingId = pending.existingEntry?.id ?: return@remember null - val path = pluginManager.pluginsDirPath / existingId.toPath() / "logo.png".toPath() + val logoPath = remember(pending.entry.id) { + val path = pluginManager.pluginsDirPath / pending.entry.id.toPath() / "logo.png".toPath() if (FileSystem.SYSTEM.exists(path)) path else null } PluginPermissionDialog( @@ -224,6 +229,144 @@ fun PluginScreen( ) } + showPluginInfo?.let { plugin -> + val logoPath = remember(plugin.id) { + val path = pluginManager.pluginsDirPath / plugin.id.toPath() / "logo.png".toPath() + if (FileSystem.SYSTEM.exists(path)) path else null + } + ThemedDialog( + onDismissRequest = { showPluginInfo = null }, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Surface( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(12.dp)), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f) + ) { + if (logoPath != null) { + val platformContext = LocalPlatformContext.current + AsyncImage( + model = ImageRequest.Builder(platformContext) + .data(logoPath.toString()) + .crossfade(true) + .build(), + contentDescription = plugin.name, + modifier = Modifier.fillMaxSize() + ) + } else { + Box(contentAlignment = Alignment.Center) { + Icon( + Iconsax.IconsaxBox, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + Text( + plugin.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + }, + actions = { + PrimaryButton(onClick = { showPluginInfo = null }) { + Text("Close") + } + } + ) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + if (plugin.description.isNotBlank()) { + Text( + plugin.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + DetailRow("Version", plugin.version) + DetailRow("API Version", plugin.apiVersion) + DetailRow("Author", plugin.author) + if (plugin.license.isNotBlank()) { + DetailRow("License", plugin.license) + } + + if (plugin.capabilities.isNotEmpty()) { + DetailChipsRow("Capabilities", plugin.capabilities.map { it.name }) + } + + if (plugin.abilities.isNotEmpty()) { + DetailChipsRow("Abilities", plugin.abilities.map { ability -> + when (ability) { + PluginAbility.METADATA -> "Metadata" + PluginAbility.AUDIO -> "Audio" + PluginAbility.LYRICS -> "Lyrics" + PluginAbility.SCROBBLE -> "Scrobble" + } + }) + } + + if (plugin.repository.isNotBlank()) { + ClickableDetailRow(label = "Repository", value = plugin.repository) { + openUrlInBrowser(plugin.repository) + } + } + + if (plugin.contact.isNotBlank()) { + DetailRow("Contact", plugin.contact) + } + + if (plugin.bugs.isNotBlank()) { + ClickableDetailRow(label = "Report Bugs", value = plugin.bugs) { + openUrlInBrowser(plugin.bugs) + } + } + } + } + } + + showPluginSupport?.let { plugin -> + ThemedDialog( + onDismissRequest = { showPluginSupport = null; supportText = null }, + title = { + Text( + "Support ${plugin.name}", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + }, + actions = { + PrimaryButton(onClick = { showPluginSupport = null; supportText = null }) { + Text("Close") + } + } + ) { + if (isLoadingSupport) { + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + Text( + supportText ?: "", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + if (showInstallSheet) { AdaptiveDialogBottomSheet( onDismiss = { showInstallSheet = false }, @@ -539,6 +682,23 @@ fun PluginScreen( }, isLoggedIn = isLoggedIn, logoPath = logoPath, + onInfo = { showPluginInfo = plugin }, + onSupport = if (selectedService != null) { + { + isLoadingSupport = true + scope.launch { + showPluginSupport = plugin + val version = SemVer.parse(plugin.version) + selectedService.use { + supportText = + coreAPI.supportMarkdownText(version) + } + isLoadingSupport = false + } + } + } else { + null + }, onLogin = if (requiresAuth && selectedService != null) { { pluginManager.launchTask { @@ -823,6 +983,78 @@ fun PluginScreen( } } +@Composable +private fun DetailRow(label: String, value: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + "$label:", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.widthIn(min = 90.dp) + ) + Text( + value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +private fun ClickableDetailRow(label: String, value: String, onClick: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + "$label:", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.widthIn(min = 90.dp) + ) + Text( + value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun DetailChipsRow(label: String, chips: List) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + "$label:", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.widthIn(min = 90.dp) + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + chips.forEach { chip -> + Surface( + shape = RoundedCornerShape(4.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.6f) + ) { + Text( + chip, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + } +} + @Composable fun DefaultAbilityPluginSelector( ability: PluginAbility, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginCard.kt index 3a688f68..ec481a72 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginCard.kt @@ -52,6 +52,8 @@ import dev.krtirtho.spotube.modules.plugin.PluginEntry import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxBox import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare +import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation +import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart import dev.krtirtho.spotube.resources.iconsax.IconsaxTag import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash import dev.krtirtho.spotube.resources.iconsax.User @@ -76,6 +78,8 @@ internal fun PluginCard( isLoggedIn: Boolean, onLogin: (() -> Unit)? = null, onLogout: (() -> Unit)? = null, + onInfo: (() -> Unit)? = null, + onSupport: (() -> Unit)? = null, logoPath: Path? = null, ) { Row( @@ -234,28 +238,50 @@ internal fun PluginCard( Column( modifier = Modifier.align(Alignment.Bottom), horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - if (plugin in BUILT_IN_PLUGINS) { - // Built-in plugins cannot be removed - Text( - stringResource(Res.string.plugin_state_builtin), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - .border( - BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), - shape = RoundedCornerShape(6.dp) + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (onInfo != null) { + GhostIconButton(onClick = onInfo) { + Icon( + Iconsax.IconsaxInformation, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) - } else { - GhostIconButton(onClick = onRemove) { - Icon( - Iconsax.IconsaxTrash, - contentDescription = stringResource(Res.string.plugin_action_remove), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + } + } + if (onSupport != null) { + GhostIconButton(onClick = onSupport) { + Icon( + Iconsax.IconsaxHeart, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (plugin in BUILT_IN_PLUGINS) { + Text( + stringResource(Res.string.plugin_state_builtin), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + .border( + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) ) + } else { + GhostIconButton(onClick = onRemove) { + Icon( + Iconsax.IconsaxTrash, + contentDescription = stringResource(Res.string.plugin_action_remove), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxInformation.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxInformation.kt new file mode 100644 index 00000000..b8b552b6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxInformation.kt @@ -0,0 +1,97 @@ +package dev.krtirtho.spotube.resources.iconsax + +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.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Iconsax.IconsaxInformation: ImageVector + get() { + if (_IconsaxInformation != null) { + return _IconsaxInformation!! + } + _IconsaxInformation = ImageVector.Builder( + name = "IconsaxInformation", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + group( + clipPathData = PathData { + moveTo(0f, 0f) + horizontalLineToRelative(24f) + verticalLineToRelative(24f) + horizontalLineToRelative(-24f) + close() + } + ) { + path( + fill = SolidColor(Color.White), + fillAlpha = 0.4f, + strokeAlpha = 0.4f + ) { + moveTo(10.75f, 2.45f) + curveTo(11.45f, 1.86f, 12.58f, 1.86f, 13.26f, 2.45f) + lineTo(14.84f, 3.8f) + curveTo(15.14f, 4.05f, 15.71f, 4.26f, 16.11f, 4.26f) + horizontalLineTo(17.81f) + curveTo(18.87f, 4.26f, 19.74f, 5.13f, 19.74f, 6.19f) + verticalLineTo(7.89f) + curveTo(19.74f, 8.29f, 19.95f, 8.85f, 20.2f, 9.15f) + lineTo(21.55f, 10.73f) + curveTo(22.14f, 11.43f, 22.14f, 12.56f, 21.55f, 13.24f) + lineTo(20.2f, 14.82f) + curveTo(19.95f, 15.12f, 19.74f, 15.68f, 19.74f, 16.08f) + verticalLineTo(17.78f) + curveTo(19.74f, 18.84f, 18.87f, 19.71f, 17.81f, 19.71f) + horizontalLineTo(16.11f) + curveTo(15.71f, 19.71f, 15.15f, 19.92f, 14.85f, 20.17f) + lineTo(13.27f, 21.52f) + curveTo(12.57f, 22.11f, 11.44f, 22.11f, 10.76f, 21.52f) + lineTo(9.18f, 20.17f) + curveTo(8.88f, 19.92f, 8.31f, 19.71f, 7.92f, 19.71f) + horizontalLineTo(6.17f) + curveTo(5.11f, 19.71f, 4.24f, 18.84f, 4.24f, 17.78f) + verticalLineTo(16.07f) + curveTo(4.24f, 15.68f, 4.04f, 15.11f, 3.79f, 14.82f) + lineTo(2.44f, 13.23f) + curveTo(1.86f, 12.54f, 1.86f, 11.42f, 2.44f, 10.73f) + lineTo(3.79f, 9.14f) + curveTo(4.04f, 8.84f, 4.24f, 8.28f, 4.24f, 7.89f) + verticalLineTo(6.2f) + curveTo(4.24f, 5.14f, 5.11f, 4.27f, 6.17f, 4.27f) + horizontalLineTo(7.9f) + curveTo(8.3f, 4.27f, 8.86f, 4.06f, 9.16f, 3.81f) + lineTo(10.75f, 2.45f) + close() + } + path(fill = SolidColor(Color.White)) { + moveTo(12f, 16.869f) + curveTo(11.45f, 16.869f, 11f, 16.419f, 11f, 15.869f) + curveTo(11f, 15.319f, 11.44f, 14.869f, 12f, 14.869f) + curveTo(12.55f, 14.869f, 13f, 15.319f, 13f, 15.869f) + curveTo(13f, 16.419f, 12.56f, 16.869f, 12f, 16.869f) + close() + } + path(fill = SolidColor(Color.White)) { + moveTo(12f, 13.721f) + curveTo(11.59f, 13.721f, 11.25f, 13.381f, 11.25f, 12.971f) + verticalLineTo(8.131f) + curveTo(11.25f, 7.721f, 11.59f, 7.381f, 12f, 7.381f) + curveTo(12.41f, 7.381f, 12.75f, 7.721f, 12.75f, 8.131f) + verticalLineTo(12.961f) + curveTo(12.75f, 13.381f, 12.42f, 13.721f, 12f, 13.721f) + close() + } + } + }.build() + + return _IconsaxInformation!! + } + +@Suppress("ObjectPropertyName") +private var _IconsaxInformation: ImageVector? = null