feat: add adaptive dialog bottom sheet component and enhance plugin installation UI

This commit is contained in:
Kingkor Roy Tirtho 2026-07-20 15:19:52 +06:00
parent 7d1a6750c3
commit c3852a1c32
6 changed files with 274 additions and 162 deletions

View File

@ -29,7 +29,10 @@
- `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system. - `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system.
- `plugin_interfaces` also has a JS target (`browser()`), used by the plugin system. - `plugin_interfaces` also has a JS target (`browser()`), used by the plugin system.
## Desktop JVM specifics ## UI component patterns
- **AdaptiveDropdownBottomSheet** (`commonMain/.../core/ui/component/AdaptiveDropdownBottomSheet.kt`): switches between `DropdownMenu` (large screen) and `ModalBottomSheet` (small screen) via `currentWindowAdaptiveInfo()`. Do NOT use expect/actual — all adaptive components that rely ONLY on Compose/Material3 APIs belong in commonMain.
- **AdaptiveDialogBottomSheet** (`commonMain/.../core/ui/component/AdaptiveDialogBottomSheet.kt`): switches between `ThemedDialog` (large screen) and `ModalBottomSheet` (small screen) via `currentWindowAdaptiveInfo()`. Same rule — keep in commonMain unless platform-specific APIs are required.
- Use `expect`/`actual` only when the component MUST use platform-specific APIs (e.g. `WindowState` for desktop window controls, native scrollbars). Pure Compose/Material3 adaptivity stays in commonMain.
- JavaFX is required; `--add-opens` flags in `compose.desktop.application.jvmArgs` must be preserved: `javafx.graphics/javafx.scene`, `javafx.graphics/com.sun.javafx.sg.prism`, `javafx.graphics/com.sun.javafx.scene`, `javafx.web/com.sun.webkit`, `javafx.media/com.sun.media.jfxmedia`, `javafx.media/com.sun.media.jfxmedia.events`. - JavaFX is required; `--add-opens` flags in `compose.desktop.application.jvmArgs` must be preserved: `javafx.graphics/javafx.scene`, `javafx.graphics/com.sun.javafx.sg.prism`, `javafx.graphics/com.sun.javafx.scene`, `javafx.web/com.sun.webkit`, `javafx.media/com.sun.media.jfxmedia`, `javafx.media/com.sun.media.jfxmedia.events`.
- JavaFX dependencies are loaded from OpenJFX with platform classifiers (win/mac/linux) resolved at configuration time via `System.getProperty("os.name")`. - JavaFX dependencies are loaded from OpenJFX with platform classifiers (win/mac/linux) resolved at configuration time via `System.getProperty("os.name")`.

View File

@ -126,6 +126,7 @@
<string name="settings_plugins_plugin_content_description">%1$s plugin</string> <string name="settings_plugins_plugin_content_description">%1$s plugin</string>
<string name="settings_plugins_clear_content_description">Clear</string> <string name="settings_plugins_clear_content_description">Clear</string>
<string name="plugin_configure_title">Configure</string>
<string name="plugin_screen_title">Plugin Manager</string> <string name="plugin_screen_title">Plugin Manager</string>
<string name="plugin_error_enter_url">Please enter a URL</string> <string name="plugin_error_enter_url">Please enter a URL</string>
<string name="plugin_error_url_scheme">URL must start with http:// or https://</string> <string name="plugin_error_url_scheme">URL must start with http:// or https://</string>

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AdaptiveDialogBottomSheet(
onDismiss: () -> Unit,
title: @Composable (() -> Unit)? = null,
modifier: Modifier = Modifier,
breakpointDp: Float = 600f,
actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
if (isLargeScreen) {
ThemedDialog(
onDismissRequest = onDismiss,
title = title,
actions = actions,
modifier = modifier,
) {
content()
}
} else {
ModalBottomSheet(
onDismissRequest = onDismiss,
dragHandle = null,
modifier = modifier,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
) {
title?.let {
it()
Spacer(Modifier.height(12.dp))
}
content()
actions?.let {
Spacer(Modifier.height(8.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Spacer(Modifier.height(8.dp))
it()
}
}
Spacer(Modifier.height(24.dp))
}
}
}

View File

@ -26,9 +26,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -55,22 +57,30 @@ import dev.krtirtho.spotube.core.ui.base.Card
import dev.krtirtho.spotube.core.ui.base.PrimaryButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.PlatformType import dev.krtirtho.spotube.PlatformType
import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
import dev.krtirtho.spotube.core.webview.WebViewController import dev.krtirtho.spotube.core.webview.WebViewController
import dev.krtirtho.spotube.getPlatform import dev.krtirtho.spotube.getPlatform
import dev.krtirtho.spotube.modules.plugin.components.InstallSection
import dev.krtirtho.spotube.modules.plugin.components.PluginCard import dev.krtirtho.spotube.modules.plugin.components.PluginCard
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft
@ -83,6 +93,9 @@ import org.koin.compose.koinInject
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.plugin_empty_subtitle import spotube.composeapp.generated.resources.plugin_empty_subtitle
import spotube.composeapp.generated.resources.plugin_action_download
import spotube.composeapp.generated.resources.plugin_action_install_from_file
import spotube.composeapp.generated.resources.plugin_configure_title
import spotube.composeapp.generated.resources.plugin_empty_title import spotube.composeapp.generated.resources.plugin_empty_title
import spotube.composeapp.generated.resources.plugin_error_download_failed import spotube.composeapp.generated.resources.plugin_error_download_failed
import spotube.composeapp.generated.resources.plugin_error_enter_url import spotube.composeapp.generated.resources.plugin_error_enter_url
@ -90,7 +103,9 @@ import spotube.composeapp.generated.resources.plugin_error_url_scheme
import spotube.composeapp.generated.resources.plugin_installed_count import spotube.composeapp.generated.resources.plugin_installed_count
import spotube.composeapp.generated.resources.plugin_installed_plural import spotube.composeapp.generated.resources.plugin_installed_plural
import spotube.composeapp.generated.resources.plugin_installed_singular import spotube.composeapp.generated.resources.plugin_installed_singular
import spotube.composeapp.generated.resources.plugin_install_section_title
import spotube.composeapp.generated.resources.plugin_screen_title import spotube.composeapp.generated.resources.plugin_screen_title
import spotube.composeapp.generated.resources.plugin_url_placeholder
import spotube.composeapp.generated.resources.settings_plugins_action_change import spotube.composeapp.generated.resources.settings_plugins_action_change
import spotube.composeapp.generated.resources.settings_plugins_action_select import spotube.composeapp.generated.resources.settings_plugins_action_select
import spotube.composeapp.generated.resources.settings_plugins_ability_audio import spotube.composeapp.generated.resources.settings_plugins_ability_audio
@ -120,6 +135,7 @@ fun PluginScreen(
var urlInput by remember { mutableStateOf("") } var urlInput by remember { mutableStateOf("") }
var urlError by remember { mutableStateOf<String?>(null) } var urlError by remember { mutableStateOf<String?>(null) }
var isLoadingUrl by remember { mutableStateOf(false) } var isLoadingUrl by remember { mutableStateOf(false) }
var showInstallSheet by remember { mutableStateOf(false) }
val pleaseEnterUrl = stringResource(Res.string.plugin_error_enter_url) val pleaseEnterUrl = stringResource(Res.string.plugin_error_enter_url)
val urlSchemeError = stringResource(Res.string.plugin_error_url_scheme) val urlSchemeError = stringResource(Res.string.plugin_error_url_scheme)
@ -175,6 +191,90 @@ fun PluginScreen(
) )
} }
if (showInstallSheet) {
AdaptiveDialogBottomSheet(
onDismiss = { showInstallSheet = false },
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Iconsax.IconsaxImportArrow2Bulk,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
stringResource(Res.string.plugin_install_section_title),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
}
},
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = urlInput,
onValueChange = { urlInput = it; urlError = null },
modifier = Modifier.weight(1f),
placeholder = {
Text(
stringResource(Res.string.plugin_url_placeholder),
style = MaterialTheme.typography.bodySmall
)
},
leadingIcon = {
Icon(
Iconsax.IconsaxLink,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
},
isError = urlError != null,
singleLine = true,
)
SecondaryIconButton(
onClick = { submitUrl() },
enabled = !isLoadingUrl,
) {
if (isLoadingUrl) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Icon(
Iconsax.IconsaxImportArrow2Bulk,
contentDescription = stringResource(Res.string.plugin_action_download),
)
}
}
}
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
OutlineButton(
onClick = { launcher.launch() },
modifier = Modifier.fillMaxWidth(),
) {
Icon(
Iconsax.IconsaxExportArrowBulk,
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.plugin_action_install_from_file))
}
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) }) ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) })
@ -205,16 +305,28 @@ fun PluginScreen(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) )
{ {
// ── Install section ─────────────────────────────────── // ── Configure header ──────────────────────────────
item { item {
InstallSection( Row(
urlInput = urlInput, modifier = Modifier
onUrlChange = { urlInput = it; urlError = null }, .fillMaxWidth()
urlError = urlError, .padding(horizontal = 4.dp, vertical = 4.dp),
isLoadingUrl = isLoadingUrl, horizontalArrangement = Arrangement.SpaceBetween,
onSubmitUrl = { submitUrl() }, verticalAlignment = Alignment.CenterVertically
onPickFile = { launcher.launch() } ) {
) Text(
stringResource(Res.string.plugin_configure_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
PrimaryButton(onClick = { showInstallSheet = true }) {
Icon(
Iconsax.IconsaxAdd,
contentDescription = "Install a plugin",
)
Text(stringResource(Res.string.plugin_install_section_title))
}
}
} }
// ── Default ability plugin selectors ───────────────── // ── Default ability plugin selectors ─────────────────

View File

@ -1,151 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.modules.plugin.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.ui.base.Card
import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
import org.jetbrains.compose.resources.stringResource
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.plugin_action_download
import spotube.composeapp.generated.resources.plugin_action_install_from_file
import spotube.composeapp.generated.resources.plugin_install_section_title
import spotube.composeapp.generated.resources.plugin_url_placeholder
@Composable
internal fun InstallSection(
urlInput: String,
onUrlChange: (String) -> Unit,
urlError: String?,
isLoadingUrl: Boolean,
onSubmitUrl: () -> Unit,
onPickFile: () -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Iconsax.IconsaxImportArrow2Bulk,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
stringResource(Res.string.plugin_install_section_title),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
}
// URL row
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = urlInput,
onValueChange = onUrlChange,
modifier = Modifier.weight(1f),
placeholder = {
Text(
stringResource(Res.string.plugin_url_placeholder),
style = MaterialTheme.typography.bodySmall
)
},
leadingIcon = {
Icon(
Iconsax.IconsaxLink,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
},
isError = urlError != null,
// supportingText = urlError?.let { { Text(it) } },
// shape = RoundedCornerShape(10.dp),
singleLine = true,
)
SecondaryIconButton(
onClick = onSubmitUrl,
enabled = !isLoadingUrl,
) {
if (isLoadingUrl) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Icon(
Iconsax.IconsaxImportArrow2Bulk,
contentDescription = stringResource(Res.string.plugin_action_download),
)
}
}
}
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
// File picker
OutlineButton(
onClick = onPickFile,
modifier = Modifier.fillMaxWidth(),
) {
Icon(
Iconsax.IconsaxExportArrowBulk,
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.plugin_action_install_from_file))
}
}
}
}

View File

@ -0,0 +1,61 @@
package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
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.IconsaxAdd: ImageVector
get() {
if (_IncosaxAdd != null) {
return _IncosaxAdd!!
}
_IncosaxAdd = ImageVector.Builder(
name = "IncosaxAdd",
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(
fillAlpha = 0.4f,
stroke = SolidColor(Color.White),
strokeAlpha = 0.4f,
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(6f, 12f)
horizontalLineTo(18f)
}
path(
stroke = SolidColor(Color.White),
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(12f, 18f)
verticalLineTo(6f)
}
}
}.build()
return _IncosaxAdd!!
}
@Suppress("ObjectPropertyName")
private var _IncosaxAdd: ImageVector? = null