Skip to content

Commit cd2df46

Browse files
committed
Refactor icon import issue validation and resolution logic, add more tests
1 parent c03bb64 commit cd2df46

7 files changed

Lines changed: 719 additions & 242 deletions

File tree

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt

Lines changed: 11 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import io.github.composegears.valkyrie.ui.foundation.picker.PickerEvent
2121
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ConversionEvent.OpenPreview
2222
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPackConversionState.BatchProcessing
2323
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPackConversionState.IconsPickering
24-
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util.checkImportIssues
24+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util.BatchIconsIssuesResolver.resolve
25+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util.BatchIconsValidator.validate
2526
import java.nio.file.Path
2627
import kotlin.io.path.isDirectory
2728
import kotlin.io.path.isRegularFile
@@ -66,7 +67,7 @@ class IconPackConversionViewModel(
6667
_state.updateState {
6768
BatchProcessing.IconPackCreationState(
6869
icons = restoredState,
69-
importIssues = restoredState.checkImportIssues(useFlatPackage = isFlatPackage),
70+
importIssues = validate(batchIcons = restoredState, useFlatPackage = isFlatPackage),
7071
)
7172
}
7273
}
@@ -93,7 +94,7 @@ class IconPackConversionViewModel(
9394
_state.updateState {
9495
when (this) {
9596
is BatchProcessing.IconPackCreationState -> {
96-
copy(importIssues = icons.checkImportIssues(useFlatPackage = settings.flatPackage))
97+
copy(importIssues = validate(batchIcons = icons, useFlatPackage = settings.flatPackage))
9798
}
9899
else -> this
99100
}
@@ -121,7 +122,7 @@ class IconPackConversionViewModel(
121122
} else {
122123
copy(
123124
icons = iconsToProcess,
124-
importIssues = iconsToProcess.checkImportIssues(useFlatPackage = isFlatPackage),
125+
importIssues = validate(batchIcons = iconsToProcess, useFlatPackage = isFlatPackage),
125126
)
126127
}
127128
}
@@ -148,7 +149,7 @@ class IconPackConversionViewModel(
148149
}
149150
copy(
150151
icons = updatedIcons,
151-
importIssues = updatedIcons.checkImportIssues(useFlatPackage = isFlatPackage),
152+
importIssues = validate(batchIcons = updatedIcons, useFlatPackage = isFlatPackage),
152153
)
153154
}
154155
else -> this
@@ -244,7 +245,7 @@ class IconPackConversionViewModel(
244245
}
245246
copy(
246247
icons = icons,
247-
importIssues = icons.checkImportIssues(useFlatPackage = isFlatPackage),
248+
importIssues = validate(icons, useFlatPackage = isFlatPackage),
248249
)
249250
}
250251
else -> this
@@ -259,99 +260,7 @@ class IconPackConversionViewModel(
259260
fun resolveImportIssues() = viewModelScope.launch {
260261
val creationState = _state.value.safeAs<BatchProcessing.IconPackCreationState>() ?: return@launch
261262

262-
val processedIcons = creationState.icons
263-
.filterIsInstance<BatchIcon.Valid>()
264-
.map { icon ->
265-
val name = icon.iconName.name
266-
267-
when {
268-
name.isEmpty() -> icon.copy(iconName = IconName("IconName"))
269-
name.contains(" ") -> icon.copy(iconName = IconName(name.replace(" ", "")))
270-
else -> icon
271-
}
272-
}
273-
274-
// Group icons by their output location (considering useFlatPackage)
275-
val iconsByLocation = processedIcons.groupBy { icon ->
276-
when (val pack = icon.iconPack) {
277-
is IconPack.Single -> pack.iconPackName
278-
is IconPack.Nested -> when {
279-
isFlatPackage -> pack.iconPackName // All in same location when flat
280-
else -> "${pack.iconPackName}.${pack.currentNestedPack}" // Separate locations
281-
}
282-
}
283-
}
284-
285-
// Process duplicates within each location group
286-
val resolvedIcons = iconsByLocation.flatMap { (_, iconsInLocation) ->
287-
// Track all committed names to ensure uniqueness across both passes
288-
val committedNames = mutableSetOf<String>()
289-
290-
// First, resolve exact duplicates
291-
val nameGroups = iconsInLocation.groupBy { it.iconName.name }
292-
val nameCounters = mutableMapOf<String, Int>()
293-
294-
val iconsWithResolvedExactDuplicates = iconsInLocation.map { icon ->
295-
val originalName = icon.iconName.name
296-
val group = nameGroups[originalName]
297-
298-
if (group != null && group.size > 1) {
299-
val counter = nameCounters.getOrDefault(originalName, 0) + 1
300-
nameCounters[originalName] = counter
301-
302-
if (counter > 1) {
303-
// Generate unique name by incrementing suffix until not in committedNames
304-
var suffix = counter - 1
305-
var candidateName = "$originalName$suffix"
306-
while (committedNames.any { it.equals(candidateName, ignoreCase = true) }) {
307-
suffix++
308-
candidateName = "$originalName$suffix"
309-
}
310-
committedNames.add(candidateName)
311-
icon.copy(iconName = IconName(candidateName))
312-
} else {
313-
committedNames.add(originalName)
314-
icon
315-
}
316-
} else {
317-
committedNames.add(originalName)
318-
icon
319-
}
320-
}
321-
322-
// Then, resolve case-insensitive duplicates
323-
val lowercaseGroups = iconsWithResolvedExactDuplicates.groupBy { it.iconName.name.lowercase() }
324-
val lowercaseCounters = mutableMapOf<String, Int>()
325-
326-
iconsWithResolvedExactDuplicates.map { icon ->
327-
val currentName = icon.iconName.name
328-
val lowercaseKey = currentName.lowercase()
329-
val group = lowercaseGroups[lowercaseKey]
330-
331-
// Only process if there are multiple icons with same lowercase name but different actual names
332-
if (group != null && group.size > 1 && group.map { it.iconName.name }.distinct().size > 1) {
333-
val counter = lowercaseCounters.getOrDefault(lowercaseKey, 0) + 1
334-
lowercaseCounters[lowercaseKey] = counter
335-
336-
if (counter > 1) {
337-
// Generate unique name by incrementing suffix until not in committedNames
338-
var suffix = counter - 1
339-
var candidateName = "$currentName$suffix"
340-
while (committedNames.any { it.equals(candidateName, ignoreCase = true) }) {
341-
suffix++
342-
candidateName = "$currentName$suffix"
343-
}
344-
committedNames.add(candidateName)
345-
icon.copy(iconName = IconName(candidateName))
346-
} else {
347-
// First in group - name is already in committedNames from exact duplicate pass
348-
icon
349-
}
350-
} else {
351-
icon
352-
}
353-
}
354-
}
263+
val resolvedIcons = resolve(batchIcons = creationState.icons, useFlatPackage = isFlatPackage)
355264

356265
if (resolvedIcons.isEmpty()) {
357266
_events.send(ConversionEvent.NothingToImport)
@@ -360,7 +269,7 @@ class IconPackConversionViewModel(
360269
_state.updateState {
361270
creationState.copy(
362271
icons = resolvedIcons,
363-
importIssues = resolvedIcons.checkImportIssues(useFlatPackage = isFlatPackage),
272+
importIssues = validate(batchIcons = resolvedIcons, useFlatPackage = isFlatPackage),
364273
)
365274
}
366275
}
@@ -388,7 +297,7 @@ class IconPackConversionViewModel(
388297

389298
BatchProcessing.IconPackCreationState(
390299
icons = icons,
391-
importIssues = icons.checkImportIssues(useFlatPackage = isFlatPackage),
300+
importIssues = validate(batchIcons = icons, useFlatPackage = isFlatPackage),
392301
)
393302
}
394303
}
@@ -427,7 +336,7 @@ class IconPackConversionViewModel(
427336

428337
BatchProcessing.IconPackCreationState(
429338
icons = icons,
430-
importIssues = icons.checkImportIssues(useFlatPackage = isFlatPackage),
339+
importIssues = validate(batchIcons = icons, useFlatPackage = isFlatPackage),
431340
)
432341
}
433342
}

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/ui/ImportIssues.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import io.github.composegears.valkyrie.sdk.compose.foundation.layout.CenterVerti
1616
import io.github.composegears.valkyrie.sdk.compose.foundation.layout.Spacer
1717
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconName
1818
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ValidationError
19-
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util.toMessageText
19+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util.BatchIconsValidator.toMessageText
2020
import io.github.composegears.valkyrie.util.stringResource
2121
import org.jetbrains.compose.ui.tooling.preview.Preview
2222
import org.jetbrains.jewel.foundation.theme.JewelTheme
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.util
2+
3+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.BatchIcon
4+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconName
5+
import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPack
6+
7+
private const val DEFAULT_ICON_NAME = "IconName"
8+
9+
object BatchIconsIssuesResolver {
10+
11+
fun resolve(batchIcons: List<BatchIcon>, useFlatPackage: Boolean = false): List<BatchIcon.Valid> {
12+
val sanitizedIcons = batchIcons
13+
.filterIsInstance<BatchIcon.Valid>()
14+
.map { it.sanitizeName() }
15+
16+
return sanitizedIcons
17+
.groupBy { it.outputLocationKey(useFlatPackage) }
18+
.flatMap { (_, iconsInLocation) -> resolveLocationDuplicates(iconsInLocation) }
19+
}
20+
21+
private fun BatchIcon.Valid.sanitizeName(): BatchIcon.Valid {
22+
val name = iconName.name
23+
return when {
24+
name.isEmpty() -> copy(iconName = IconName(DEFAULT_ICON_NAME))
25+
name.contains(" ") -> copy(iconName = IconName(name.replace(" ", "")))
26+
else -> this
27+
}
28+
}
29+
30+
private fun resolveLocationDuplicates(icons: List<BatchIcon.Valid>): List<BatchIcon.Valid> {
31+
val committedNames = UniqueNameTracker(initialNames = icons.map { it.iconName.name })
32+
33+
val afterExactDedup = resolveExactDuplicates(icons, committedNames)
34+
return resolveCaseInsensitiveDuplicates(afterExactDedup, committedNames)
35+
}
36+
37+
private fun resolveExactDuplicates(
38+
icons: List<BatchIcon.Valid>,
39+
committedNames: UniqueNameTracker,
40+
): List<BatchIcon.Valid> {
41+
val nameGroups = icons.groupBy { it.iconName.name }
42+
val nameCounters = mutableMapOf<String, Int>()
43+
44+
return icons.map { icon ->
45+
val originalName = icon.iconName.name
46+
val isDuplicate = nameGroups[originalName].orEmpty().size > 1
47+
48+
if (!isDuplicate) return@map icon
49+
50+
val counter = nameCounters.increment(originalName)
51+
if (counter == 1) return@map icon
52+
53+
val uniqueName = committedNames.generateUniqueName(baseName = originalName, startSuffix = counter - 1)
54+
icon.copy(iconName = IconName(uniqueName))
55+
}
56+
}
57+
58+
private fun resolveCaseInsensitiveDuplicates(
59+
icons: List<BatchIcon.Valid>,
60+
committedNames: UniqueNameTracker,
61+
): List<BatchIcon.Valid> {
62+
val lowercaseGroups = icons.groupBy { it.iconName.name.lowercase() }
63+
val lowercaseCounters = mutableMapOf<String, Int>()
64+
65+
return icons.map { icon ->
66+
val currentName = icon.iconName.name
67+
val lowercaseKey = currentName.lowercase()
68+
val group = lowercaseGroups[lowercaseKey].orEmpty()
69+
val hasCaseConflict = group.size > 1 && group.distinctBy { it.iconName.name }.size > 1
70+
71+
if (!hasCaseConflict) return@map icon
72+
73+
val counter = lowercaseCounters.increment(lowercaseKey)
74+
if (counter == 1) return@map icon
75+
76+
val uniqueName = committedNames.generateUniqueName(baseName = currentName, startSuffix = counter - 1)
77+
icon.copy(iconName = IconName(uniqueName))
78+
}
79+
}
80+
81+
private fun BatchIcon.Valid.outputLocationKey(useFlatPackage: Boolean): String {
82+
return when (val pack = iconPack) {
83+
is IconPack.Single -> pack.iconPackName
84+
is IconPack.Nested -> when {
85+
useFlatPackage -> pack.iconPackName
86+
else -> "${pack.iconPackName}.${pack.currentNestedPack}"
87+
}
88+
}
89+
}
90+
}
91+
92+
private class UniqueNameTracker(initialNames: List<String>) {
93+
94+
private val names = initialNames.toMutableSet()
95+
96+
fun generateUniqueName(baseName: String, startSuffix: Int): String {
97+
var suffix = startSuffix
98+
var candidate = "$baseName$suffix"
99+
while (names.any { it.equals(candidate, ignoreCase = true) }) {
100+
suffix++
101+
candidate = "$baseName$suffix"
102+
}
103+
names.add(candidate)
104+
return candidate
105+
}
106+
}
107+
108+
private fun MutableMap<String, Int>.increment(key: String): Int {
109+
val newValue = getOrDefault(key, 0) + 1
110+
this[key] = newValue
111+
return newValue
112+
}

0 commit comments

Comments
 (0)