rewrite on multiplatform

This commit is contained in:
2021-03-01 20:17:06 +06:00
parent 376691ef74
commit f1595bb5ed
36 changed files with 347 additions and 108 deletions

37
desktop/build.gradle Normal file
View File

@@ -0,0 +1,37 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id("org.jetbrains.compose") version "$compose_version"
}
apply from: "$mppJavaProjectPresetPath"
kotlin {
jvm {
compilations.main.kotlinOptions {
jvmTarget = "11"
useIR = true
}
}
sourceSets {
commonMain {
dependencies {
implementation project(":kmppscriptbuilder.core")
}
}
jvmMain {
dependencies {
implementation(compose.desktop.currentOs)
api "io.ktor:ktor-client-cio:$ktor_version"
}
}
}
}
compose.desktop {
application {
mainClass = "dev.inmo.kmppscriptbuilder.desktop.BuilderKt"
}
}

View File

@@ -0,0 +1,62 @@
package dev.inmo.kmppscriptbuilder.desktop
import androidx.compose.desktop.Window
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.inmo.kmppscriptbuilder.desktop.utils.*
import dev.inmo.kmppscriptbuilder.desktop.views.*
import java.io.File
//private val uncaughtExceptionsBC = BroadcastChannel<DefaultErrorHandler.ErrorEvent>(Channel.CONFLATED)
//val uncaughtExceptionsFlow: Flow<DefaultErrorHandler.ErrorEvent> = uncaughtExceptionsBC.asFlow()
fun main(args: Array<String>) = Window(title = "Kotlin Multiplatform Publishing Builder") {
val builder = BuilderView()
MaterialTheme(
Colors(
primary = Color(0x01, 0x57, 0x9b),
primaryVariant = Color(0x00, 0x2f, 0x6c),
secondary = Color(0xb2, 0xeb, 0xf2),
secondaryVariant = Color(0x81, 0xb9, 0xbf),
background = Color(0xe1, 0xe2, 0xe1),
surface = Color(0xf5, 0xf5, 0xf6),
error = Color(0xb7, 0x1c, 0x1c),
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
onError = Color.White,
isLight = MaterialTheme.colors.isLight,
)
) {
Box(
Modifier.fillMaxSize()
.background(color = Color(245, 245, 245))
) {
val stateVertical = rememberScrollState(0)
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
) {
builder.init()
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
}
}
if (args.isNotEmpty()) {
val config = loadConfigFile(File(args.first()))
builder.config = config
}
}

View File

@@ -0,0 +1,76 @@
package dev.inmo.kmppscriptbuilder.desktop.utils
import dev.inmo.kmppscriptbuilder.core.models.Config
import dev.inmo.kmppscriptbuilder.core.utils.serialFormat
import java.io.File
import javax.swing.JFileChooser
private const val appExtension = "kpsb"
private var lastFile: File? = null
fun loadConfigFile(file: File): Config {
lastFile = file
return serialFormat.decodeFromString(Config.serializer(), file.readText())
}
fun loadConfig(): Config? {
val fc = JFileChooser(lastFile ?.parent)
fc.addChoosableFileFilter(FileFilter("Kotlin Publication Scripts Builder", Regex(".*\\.$appExtension")))
fc.addChoosableFileFilter(FileFilter("JSON", Regex(".*\\.json")))
return when (fc.showOpenDialog(null)) {
JFileChooser.APPROVE_OPTION -> {
val file = fc.selectedFile
lastFile = file
return serialFormat.decodeFromString(Config.serializer(), fc.selectedFile.readText())
}
else -> null
}
}
fun saveConfig(config: Config): Boolean {
return lastFile ?.also {
it.writeText(serialFormat.encodeToString(Config.serializer(), config))
} != null
}
fun exportGradle(config: Config): Boolean {
val fc = JFileChooser(lastFile ?.parent)
fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
return when (fc.showSaveDialog(null)) {
JFileChooser.APPROVE_OPTION -> {
val file = fc.selectedFile
val mavenConfigContent = config.type.buildMavenGradleConfig(
config.mavenConfig,
config.licenses
)
File(file, "publish.gradle").apply {
delete()
createNewFile()
writeText(mavenConfigContent)
}
true
}
else -> false
}
}
fun saveAs(config: Config): Boolean {
val fc = JFileChooser(lastFile ?.parent)
fc.addChoosableFileFilter(FileFilter("Kotlin Publication Scripts Builder", Regex(".*\\.$appExtension")))
fc.addChoosableFileFilter(FileFilter("JSON", Regex(".*\\.json")))
return when (fc.showSaveDialog(null)) {
JFileChooser.APPROVE_OPTION -> {
val file = fc.selectedFile
val resultFile = if (file.extension == "") {
File(file.parentFile, "${file.name}.$appExtension")
} else {
file
}
resultFile.writeText(serialFormat.encodeToString(Config.serializer(), config))
lastFile = resultFile
true
}
else -> false
}
}

View File

@@ -0,0 +1,16 @@
package dev.inmo.kmppscriptbuilder.desktop.utils
import java.io.File
import javax.swing.filechooser.FileFilter
fun FileFilter(description: String, fileFilter: (File) -> Boolean) = object : FileFilter() {
override fun accept(f: File?): Boolean {
return fileFilter(f ?: return false)
}
override fun getDescription(): String = description
}
fun FileFilter(description: String, nameRegex: Regex) = FileFilter(description) {
it.name.matches(nameRegex)
}

View File

@@ -0,0 +1,60 @@
package dev.inmo.kmppscriptbuilder.desktop.utils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
val commonTextFieldTextStyle = TextStyle(
fontSize = 12.sp
)
@Composable
inline fun TitleText(text: String) = Text(
text, Modifier.padding(0.dp, 8.dp), fontSize = 18.sp
)
@Composable
inline fun CommonText(text: String, modifier: Modifier = Modifier) = Text(
text, modifier = modifier
)
@Composable
inline fun CommonTextField(presetText: String, hint: String, noinline onChange: (String) -> Unit) = OutlinedTextField(
presetText,
onChange,
Modifier.fillMaxWidth(),
singleLine = true,
label = {
CommonText(hint)
}
)
@Composable
inline fun SwitchWithLabel(
label: String,
checked: Boolean,
placeSwitchAtTheStart: Boolean = false,
switchEnabled: Boolean = true,
modifier: Modifier = Modifier.padding(0.dp, 8.dp),
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
switchModifier: Modifier = Modifier.padding(8.dp, 0.dp),
noinline onCheckedChange: (Boolean) -> Unit
) = Row(modifier, horizontalArrangement, verticalAlignment) {
val switchCreator = @Composable {
Switch(checked, onCheckedChange, switchModifier, enabled = switchEnabled)
}
if (placeSwitchAtTheStart) {
switchCreator()
}
CommonText(label, Modifier.align(Alignment.CenterVertically).clickable { })
if (!placeSwitchAtTheStart) {
switchCreator()
}
}

View File

@@ -0,0 +1,34 @@
package dev.inmo.kmppscriptbuilder.desktop.utils
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
abstract class View {
protected open val defaultModifier = Modifier.fillMaxWidth().padding(8.dp)
@Composable
abstract fun build()
}
abstract class VerticalView(val title: String) : View() {
abstract val content: @Composable ColumnScope.() -> Unit
@Composable
override fun build() {
TitleText(title)
Column(
defaultModifier
) {
content()
}
Spacer(Modifier.fillMaxWidth().height(8.dp))
}
}
@Composable
fun <T : View> T.init(): T = apply {
build()
}

View File

@@ -0,0 +1,103 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.svgResource
import androidx.compose.ui.unit.dp
import dev.inmo.kmppscriptbuilder.core.models.Config
import dev.inmo.kmppscriptbuilder.desktop.utils.*
class BuilderView : View() {
private val projectTypeView = ProjectTypeView()
private val mavenInfoView = MavenInfoView()
private val licensesView = LicensesView()
private var saveAvailableState by mutableStateOf(false)
override val defaultModifier: Modifier = Modifier.fillMaxSize()
var config: Config
get() = Config(licensesView.licenses, mavenInfoView.mavenConfig, projectTypeView.projectType)
set(value) {
licensesView.licenses = value.licenses
mavenInfoView.mavenConfig = value.mavenConfig
projectTypeView.projectType = value.type
saveAvailableState = true
}
@Composable
override fun build() {
Box(Modifier.fillMaxSize()) {
Column() {
TopAppBar(
@Composable {
CommonText("Kotlin publication scripts builder", Modifier.clickable { println(config) })
},
actions = {
IconButton(
{
loadConfig()?.also {
config = it
}
}
) {
Image(
painter = svgResource("images/open_file.svg"),
contentDescription = "Open file"
)
}
if (saveAvailableState) {
IconButton(
{
saveConfig(config)
}
) {
Image(
painter = svgResource("images/save_file.svg"),
contentDescription = "Save file"
)
}
}
if (saveAvailableState) {
IconButton(
{
exportGradle(config)
}
) {
Image(
painter = svgResource("images/export_gradle.svg"),
contentDescription = "Export Gradle script"
)
}
}
IconButton(
{
if (saveAs(config)) {
saveAvailableState = true
}
}
) {
Image(
painter = svgResource("images/save_as.svg"),
contentDescription = "Export Gradle script"
)
}
}
)
Column(Modifier.padding(8.dp)) {
projectTypeView.init()
Divider()
licensesView.init()
Divider()
mavenInfoView.init()
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.runtime.*
import dev.inmo.kmppscriptbuilder.core.models.Developer
import dev.inmo.kmppscriptbuilder.desktop.utils.*
class DeveloperState(
id: String = "",
name: String = "",
eMail: String = ""
) {
var id: String by mutableStateOf(id)
var name: String by mutableStateOf(name)
var eMail: String by mutableStateOf(eMail)
fun toDeveloper() = Developer(id, name, eMail)
}
private fun Developer.toDeveloperState() = DeveloperState(id, name, eMail)
class DevelopersView : ListView<DeveloperState>("Developers info") {
var developers: List<Developer>
get() = itemsList.map { it.toDeveloper() }
set(value) {
itemsList.clear()
itemsList.addAll(
value.map { it.toDeveloperState() }
)
}
override val addItemText: String = "Add developer"
override val removeItemText: String = "Remove developer"
override fun createItem(): DeveloperState = DeveloperState()
@Composable
override fun buildView(item: DeveloperState) {
CommonTextField(
item.id,
"Developer username"
) { item.id = it }
CommonTextField(
item.name,
"Developer name"
) { item.name = it }
CommonTextField(
item.eMail,
"Developer E-Mail"
) { item.eMail = it }
}
}

View File

@@ -0,0 +1,95 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.inmo.kmppscriptbuilder.core.models.License
import dev.inmo.kmppscriptbuilder.core.models.getLicenses
import dev.inmo.kmppscriptbuilder.desktop.utils.*
import io.ktor.client.HttpClient
import kotlinx.coroutines.*
private class LicenseState(
id: String = "",
title: String = "",
url: String? = null
) {
var id: String by mutableStateOf(id)
var title: String by mutableStateOf(title)
var url: String? by mutableStateOf(url)
fun toLicense() = License(id, title, url)
}
private fun License.toLicenseState() = LicenseState(id, title, url)
class LicensesView: VerticalView("Licenses") {
private var licensesListState = mutableStateListOf<LicenseState>()
var licenses: List<License>
get() = licensesListState.map { it.toLicense() }
set(value) {
licensesListState.clear()
licensesListState.addAll(value.map { it.toLicenseState() })
}
private val availableLicensesState = mutableStateListOf<License>()
private val licensesOffersToShow = mutableStateListOf<License>()
private var licenseSearchFilter by mutableStateOf("")
init {
CoroutineScope(Dispatchers.Default).launch {
val client = HttpClient()
availableLicensesState.addAll(client.getLicenses().values)
client.close()
}
}
override val content: @Composable ColumnScope.() -> Unit = {
CommonTextField(licenseSearchFilter, "Search filter") { filterText ->
licenseSearchFilter = filterText
licensesOffersToShow.clear()
if (licenseSearchFilter.isNotEmpty()) {
licensesOffersToShow.addAll(
availableLicensesState.filter { filterText.all { symbol -> symbol.toLowerCase() in it.title } }
)
}
}
Column {
licensesOffersToShow.forEach {
Column(Modifier.padding(16.dp, 8.dp, 8.dp, 8.dp)) {
CommonText(it.title, Modifier.clickable {
licensesListState.add(it.toLicenseState())
licenseSearchFilter = ""
licensesOffersToShow.clear()
})
Divider()
}
}
}
Button({ licensesListState.add(LicenseState()) }, Modifier.padding(8.dp)) {
CommonText("Add empty license")
}
licensesListState.forEach { license ->
Column(Modifier.padding(8.dp)) {
CommonTextField(
license.id,
"License ID"
) { license.id = it }
CommonTextField(
license.title,
"License title"
) { license.title = it }
CommonTextField(
license.url ?: "",
"License URL"
) { license.url = it }
Button({ licensesListState.remove(license) }, Modifier.padding(8.dp)) {
CommonText("Remove")
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.inmo.kmppscriptbuilder.desktop.utils.*
abstract class ListView<T>(title: String) : VerticalView(title) {
protected val itemsList = mutableStateListOf<T>()
protected open val addItemText: String = "Add"
protected open val removeItemText: String = "Remove"
protected abstract fun createItem(): T
@Composable
protected abstract fun buildView(item: T)
override val content: @Composable ColumnScope.() -> Unit = {
Button({ itemsList.add(createItem()) }) {
CommonText(addItemText)
}
itemsList.forEach { item ->
Column(Modifier.padding(8.dp)) {
buildView(item)
Button({ itemsList.remove(item) }, Modifier.padding(8.dp)) {
CommonText(removeItemText)
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.*
import dev.inmo.kmppscriptbuilder.core.models.MavenConfig
import dev.inmo.kmppscriptbuilder.core.models.SonatypeRepository
import dev.inmo.kmppscriptbuilder.desktop.utils.*
class MavenInfoView : VerticalView("Project information") {
private var projectNameProperty by mutableStateOf("")
private var projectDescriptionProperty by mutableStateOf("")
private var projectUrlProperty by mutableStateOf("")
private var projectVcsUrlProperty by mutableStateOf("")
private var includeGpgSignProperty by mutableStateOf(true)
private var publishToMavenCentralProperty by mutableStateOf(false)
private val developersView = DevelopersView()
private val repositoriesView = RepositoriesView()
var mavenConfig: MavenConfig
get() = MavenConfig(
projectNameProperty,
projectDescriptionProperty,
projectUrlProperty,
projectVcsUrlProperty,
includeGpgSignProperty,
developersView.developers,
repositoriesView.repositories + if (publishToMavenCentralProperty) {
listOf(SonatypeRepository)
} else {
emptyList()
}
)
set(value) {
projectNameProperty = value.name
projectDescriptionProperty = value.description
projectUrlProperty = value.url
projectVcsUrlProperty = value.vcsUrl
includeGpgSignProperty = value.includeGpgSigning
publishToMavenCentralProperty = value.repositories.any { it == SonatypeRepository }
developersView.developers = value.developers
repositoriesView.repositories = value.repositories.filter { it != SonatypeRepository }
// developersView.developers = value.developers
}
override val content: @Composable ColumnScope.() -> Unit = {
CommonTextField(
projectNameProperty,
"Public project name"
) { projectNameProperty = it }
CommonTextField(
projectDescriptionProperty,
"Public project description"
) { projectDescriptionProperty = it }
CommonTextField(
projectUrlProperty,
"Public project URL"
) { projectUrlProperty = it }
CommonTextField(
projectVcsUrlProperty,
"Public project VCS URL (with .git)"
) { projectVcsUrlProperty = it }
SwitchWithLabel(
"Include GPG Signing",
includeGpgSignProperty,
placeSwitchAtTheStart = true
) { includeGpgSignProperty = it }
SwitchWithLabel(
"Include publication to MavenCentral",
publishToMavenCentralProperty,
placeSwitchAtTheStart = true
) { publishToMavenCentralProperty = it }
developersView.init()
repositoriesView.init()
}
}

View File

@@ -0,0 +1,33 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.foundation.layout.*
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.inmo.kmppscriptbuilder.core.models.*
import dev.inmo.kmppscriptbuilder.desktop.utils.VerticalView
class ProjectTypeView : VerticalView("Project type") {
private var projectTypeState by mutableStateOf<Boolean>(false)
private val calculatedProjectType: ProjectType
get() = if (projectTypeState) JVMProjectType else MultiplatformProjectType
var projectType: ProjectType
get() = calculatedProjectType
set(value) {
projectTypeState = value == JVMProjectType
}
override val content: @Composable ColumnScope.() -> Unit = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Text("Multiplatform", Modifier.alignByBaseline())
Switch(
projectTypeState,
{ projectTypeState = it },
Modifier.padding(4.dp, 0.dp)
)
Text("JVM", Modifier.alignByBaseline())
}
}
}

View File

@@ -0,0 +1,45 @@
package dev.inmo.kmppscriptbuilder.desktop.views
import androidx.compose.runtime.*
import dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository
import dev.inmo.kmppscriptbuilder.desktop.utils.*
class RepositoryState(
name: String = "",
url: String = ""
) {
var name: String by mutableStateOf(name)
var url: String by mutableStateOf(url)
fun toRepository() = MavenPublishingRepository(name, url)
}
private fun MavenPublishingRepository.toRepositoryState() = RepositoryState(name, url)
class RepositoriesView : ListView<RepositoryState>("Repositories info") {
var repositories: List<MavenPublishingRepository>
get() = itemsList.map { it.toRepository() }
set(value) {
itemsList.clear()
itemsList.addAll(
value.map { it.toRepositoryState() }
)
}
override val addItemText: String = "Add repository"
override val removeItemText: String = "Remove repository"
override fun createItem(): RepositoryState = RepositoryState()
@Composable
override fun buildView(item: RepositoryState) {
CommonTextField(
item.name,
"Repository name"
) { item.name = it }
CommonTextField(
item.url,
"Repository url"
) { item.url = it }
}
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M18 20H6V4H13V9H18V20M16 11V18.1L13.9 16L11.1 18.8L8.3 16L11.1 13.2L8.9 11H16Z" /></svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M6.1,10L4,18V8H21A2,2 0 0,0 19,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H19C19.9,20 20.7,19.4 20.9,18.5L23.2,10H6.1M19,18H6L7.6,12H20.6L19,18Z" /></svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M4 19H10V21H4C2.89 21 2 20.1 2 19V5C2 3.9 2.89 3 4 3H16L20 7V9.12L18 11.12V7.83L15.17 5H4V19M14 10V6H5V10H14M20.42 12.3C20.31 12.19 20.18 12.13 20.04 12.13C19.9 12.13 19.76 12.19 19.65 12.3L18.65 13.3L20.7 15.35L21.7 14.35C21.92 14.14 21.92 13.79 21.7 13.58L20.42 12.3M12 19.94V22H14.06L20.12 15.93L18.07 13.88L12 19.94M14 15C14 13.34 12.66 12 11 12S8 13.34 8 15 9.34 18 11 18C11.04 18 11.08 18 11.13 18L14 15.13C14 15.09 14 15.05 14 15" /></svg>

After

Width:  |  Height:  |  Size: 744 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3M19 19H5V5H16.17L19 7.83V19M12 12C10.34 12 9 13.34 9 15S10.34 18 12 18 15 16.66 15 15 13.66 12 12 12M6 6H15V10H6V6Z" /></svg>

After

Width:  |  Height:  |  Size: 502 B