Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow rename icons during pack creation #22

Merged
merged 4 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
- Multi edit icon pack
- Generated code as val or lazy delegate
- Rename icon in icon pack
- Preview icon on square background pattern
- Simple mode without package
- Add version into plugin settings
- Export/import iconpack configuration into file
- Optimize current icons (clean up, adding into pack)
- Generate preview for whole icon pack
- Add limitations for ImageVector
- Update Readme demo with latest plugin design
- Continues drag & drop
- Add changelog into plugin
2 changes: 2 additions & 0 deletions components/parser/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ dependencies {

implementation(libs.android.build.tools)
implementation(libs.kotlin.io)

testImplementation(libs.kotlin.test)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object IconParser {
fun toVector(file: File): IconParserOutput {
val iconType = IconTypeParser.getIconType(file.extension) ?: error("File not SVG or XML")

val fileName = getFileName(fileName = file.name, iconType = iconType)
val fileName = getIconName(fileName = file.name)
val icon = when (iconType) {
SVG -> {
val tmpFile = createTempFile(suffix = "valkyrie/")
Expand All @@ -38,21 +38,14 @@ object IconParser {
)
}

private fun getFileName(fileName: String, iconType: IconType): String {

var name = fileName
.removeSuffix(".${iconType.extension}")
.split("_")
.joinToString("") { it.capitalized() }
.replace("\\d".toRegex(), "")

if (name.startsWith("ic", ignoreCase = true)) {
name = name.drop(2).capitalized()
}
return name
}
}

private fun String.capitalized(): String = replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
fun getIconName(fileName: String) = fileName
.removePrefix("-")
.removePrefix("_")
.removeSuffix(".svg")
.removeSuffix(".xml")
.removePrefix("ic_")
.removePrefix("ic-")
.replace("[^a-zA-Z0-9\\-_ ]".toRegex(), "_")
.split("_", "-")
.joinToString(separator = "") { it.lowercase().capitalized() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.composegears.valkyrie.parser

import java.util.*

fun String.removePrefix(prefix: CharSequence): String {
if (startsWith(prefix, ignoreCase = true)) {
return substring(prefix.length)
}
return this
}

fun String.capitalized(): String = replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.composegears.valkyrie.parser

import kotlin.test.Test
import kotlin.test.assertEquals

class IconParserTest {

private data class IconTest(
val fileName: String,
val expected: String
)

@Test
fun `test icon name`() {
val fileNames = listOf(
IconTest(fileName = "ic_test_icon.svg", expected = "TestIcon"),
IconTest(fileName = "ic_test_icon.xml", expected = "TestIcon"),
IconTest(fileName = "test_icon.svg", expected = "TestIcon"),
IconTest(fileName = "ic_test_icon2.svg", expected = "TestIcon2"),
IconTest(fileName = "ic_test_icon_name.svg", expected = "TestIconName"),
IconTest(fileName = "ic_testicon.svg", expected = "Testicon"),
IconTest(fileName = "ic_test_icon_name_with_underscores.svg", expected = "TestIconNameWithUnderscores"),
IconTest(fileName = "ic_TESTIcon.svg", expected = "Testicon"),
IconTest(fileName = "ic-test-icon.svg", expected = "TestIcon"),
IconTest(fileName = "ic_test@icon!.svg", expected = "TestIcon"),
IconTest(fileName = "ic_test_icon123.xml", expected = "TestIcon123"),
IconTest(fileName = "my_icon.xml", expected = "MyIcon"),
IconTest(fileName = "Ic_TeSt123Icon.svg", expected = "Test123icon"),
IconTest(fileName = "ic_special@#\$%^&*()icon.svg", expected = "SpecialIcon"),
IconTest(fileName = "ic--test__icon---name.svg", expected = "TestIconName"),
IconTest(fileName = "@#$%.svg", expected = ""),
IconTest(fileName = "", expected = ""),
IconTest(fileName = "-_ic_test_icon_-.svg", expected = "TestIcon"),
IconTest(fileName = "pos_1", expected = "Pos1"),
IconTest(fileName = "1", expected = "1"),
)

fileNames.forEach {
val iconName = IconParser.getIconName(it.fileName)

assertEquals(expected = it.expected, actual = iconName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package io.github.composegears.valkyrie.ui.foundation

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BaseTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current.copy(
fontSize = MaterialTheme.typography.bodyMedium.fontSize
),
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = true,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = RoundedCornerShape(8.dp),
colors: TextFieldColors = TextFieldDefaults.colors().copy(
focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
cursorColor = MaterialTheme.colorScheme.onSurfaceVariant,
errorTextColor = MaterialTheme.colorScheme.onError,
errorContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.5f),
errorCursorColor = MaterialTheme.colorScheme.onError,
errorTrailingIconColor = MaterialTheme.colorScheme.onError,
errorIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
) {
val textColor = textStyle.color.takeOrElse {
colors.textColor(enabled, isError, interactionSource).value
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
value = value,
modifier = modifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight
),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(colors.cursorColor(isError).value),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.DecorationBox(
contentPadding = PaddingValues(horizontal = 8.dp),
value = value,
visualTransformation = visualTransformation,
innerTextField = innerTextField,
placeholder = placeholder,
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
shape = shape,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
)
}
)
}
}

@Composable
private fun TextFieldColors.cursorColor(isError: Boolean): State<Color> {
return rememberUpdatedState(if (isError) errorCursorColor else cursorColor)
}

@Composable
private fun TextFieldColors.textColor(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource
): State<Color> {
val focused by interactionSource.collectIsFocusedAsState()

val targetValue = when {
!enabled -> disabledTextColor
isError -> errorTextColor
focused -> focusedTextColor
else -> unfocusedTextColor
}
return rememberUpdatedState(targetValue)
}

@Preview
@Composable
private fun BaseTextFieldPreview() = PreviewTheme {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
BaseTextField(
value = "Hello, World!",
onValueChange = {},
isError = false
)
BaseTextField(
value = "Hello, World!",
onValueChange = {},
isError = true
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
Expand All @@ -27,14 +28,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import com.composegears.tiamat.koin.koinTiamatViewModel
import com.composegears.tiamat.navController
import com.composegears.tiamat.navDestination
import com.composegears.tiamat.navigationSlideInOut
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.vfs.VirtualFileManager
import io.github.composegears.valkyrie.settings.ValkyriesSettings
import io.github.composegears.valkyrie.ui.foundation.AppBarTitle
import io.github.composegears.valkyrie.ui.foundation.ClearAction
import io.github.composegears.valkyrie.ui.foundation.SettingsAction
Expand All @@ -54,7 +56,6 @@ val IconPackConversionScreen by navDestination<Unit> {

val viewModel = koinTiamatViewModel<IconPackConversionViewModel>()
val state by viewModel.state.collectAsState()
val settings by viewModel.valkyriesSettings.collectAsState()

LaunchedEffect(Unit) {
viewModel.events
Expand All @@ -77,7 +78,6 @@ val IconPackConversionScreen by navDestination<Unit> {

IconPackConversionUi(
state = state,
settings = settings,
openSettings = {
navController.navigate(
dest = SettingsScreen,
Expand All @@ -89,21 +89,22 @@ val IconPackConversionScreen by navDestination<Unit> {
onDeleteIcon = viewModel::deleteIcon,
onReset = viewModel::reset,
onPreviewClick = viewModel::showPreview,
onExport = viewModel::export
onExport = viewModel::export,
onRenameIcon = viewModel::renameIcon
)
}

@Composable
private fun IconPackConversionUi(
state: IconPackConversionState,
settings: ValkyriesSettings,
openSettings: () -> Unit,
onPickEvent: (PickerEvent) -> Unit,
updatePack: (BatchIcon, String) -> Unit,
onDeleteIcon: (IconName) -> Unit,
onReset: () -> Unit,
onPreviewClick: (IconName) -> Unit,
onExport: () -> Unit
onExport: () -> Unit,
onRenameIcon: (BatchIcon, IconName) -> Unit
) {
var isVisible by rememberSaveable { mutableStateOf(true) }

Expand All @@ -122,7 +123,13 @@ private fun IconPackConversionUi(
}
}

Box {
val focusManager = LocalFocusManager.current

Box(modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(onTap = { focusManager.clearFocus() })
}
) {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar {
if (state is BatchFilesProcessing) {
Expand All @@ -142,7 +149,8 @@ private fun IconPackConversionUi(
icons = state.iconsToProcess,
onDeleteIcon = onDeleteIcon,
onUpdatePack = updatePack,
onPreviewClick = onPreviewClick
onPreviewClick = onPreviewClick,
onRenameIcon = onRenameIcon
)
}
}
Expand Down
Loading