Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
70b16db
Align plugin settings with capability parameters
matteomekhail Aug 14, 2026
485a9f3
Add safe directory operations to scoped filesystems
matteomekhail Aug 14, 2026
e3535b4
Resolve custom plugin defaults consistently
matteomekhail Aug 14, 2026
dea9ce8
Divide required and optional plugin settings
matteomekhail Aug 14, 2026
11b5294
Rework color picker dialog
matteomekhail Aug 14, 2026
d343ba9
fix: complete setting metadata integration
matteomekhail Aug 14, 2026
848eb8d
fix: harden sandboxed file deletion
matteomekhail Aug 14, 2026
2eff259
fix: preserve unset plugin settings
matteomekhail Aug 14, 2026
badf10c
fix: test setting groups and target deep links
matteomekhail Aug 14, 2026
5062c9d
Merge branch 'codex/issue-3-plugin-setting-parameters' into codex/iss…
matteomekhail Aug 14, 2026
d75c541
Merge branch 'codex/issue-11-custom-setting-defaults' into codex/issu…
matteomekhail Aug 14, 2026
5cc8dcc
fix: preserve color picker values and alpha
matteomekhail Aug 14, 2026
9d7e9d5
fix: resolve generated settings safely at runtime
matteomekhail Aug 14, 2026
4a1158f
fix: preserve legacy color values and alpha
matteomekhail Aug 14, 2026
144d594
fix: preserve safe paths and isolate cache operations
matteomekhail Aug 14, 2026
0eb3aaf
Merge branch 'codex/issue-3-plugin-setting-parameters' into codex/iss…
matteomekhail Aug 14, 2026
153c666
fix: preserve unchanged settings cache identity
matteomekhail Aug 14, 2026
5768bef
fix: retain alpha for functional color values
matteomekhail Aug 14, 2026
a9f0142
fix: handle unavailable sandbox roots safely
matteomekhail Aug 14, 2026
894a2eb
fix: round-trip functional and short alpha colors
matteomekhail Aug 14, 2026
2f15ae1
fix: apply manifest defaults to capability gates
matteomekhail Aug 14, 2026
c6cb1eb
Merge branch 'codex/issue-11-custom-setting-defaults' into codex/issu…
matteomekhail Aug 14, 2026
ffaf22e
Merge branch 'codex/issue-12-divide-plugin-settings' into codex/issue…
matteomekhail Aug 14, 2026
e203a5c
fix: keep cache filesystem operations isolated
matteomekhail Aug 15, 2026
ce2f1a8
Merge branch 'codex/issue-10-plugin-filesystem' into codex/issue-11-c…
matteomekhail Aug 15, 2026
db3dfd2
Merge branch 'codex/issue-11-custom-setting-defaults' into codex/issu…
matteomekhail Aug 15, 2026
092c09e
Merge branch 'codex/issue-12-divide-plugin-settings' into codex/issue…
matteomekhail Aug 15, 2026
62f0587
Revert "Merge branch 'codex/issue-12-divide-plugin-settings' into cod…
matteomekhail Aug 15, 2026
71d1596
Revert "Merge branch 'codex/issue-12-divide-plugin-settings' into cod…
matteomekhail Aug 15, 2026
0035918
Merge remote-tracking branch 'origin/dev' into codex/issue-13-color-p…
matteomekhail Aug 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@
<string name="action_pause">Pausa</string>
<string name="action_save">Salva</string>
<string name="action_cancel">Annulla</string>
<string name="color_picker_title">Scegli un colore</string>
<string name="color_picker_hex">Esadecimale</string>
<string name="color_picker_hex_hint">Usa #RRGGBB o #AARRGGBB</string>
<string name="color_picker_apply">Applica</string>
<string name="action_expand">Espandi</string>
<string name="action_collapse">Comprimi</string>
<string name="plugin_settings_title">Impostazioni: %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@
<string name="action_pause">Pause</string>
<string name="action_save">Save</string>
<string name="action_cancel">Cancel</string>
<string name="color_picker_title">Choose a color</string>
<string name="color_picker_hex">Hex</string>
<string name="color_picker_hex_hint">Use #RRGGBB or #AARRGGBB</string>
<string name="color_picker_apply">Apply</string>
<string name="action_expand">Expand</string>
<string name="action_collapse">Collapse</string>
<string name="plugin_settings_title">Settings: %1$s</string>
Expand Down Expand Up @@ -474,4 +478,4 @@
<string name="repo_filter_label">Filter:</string>
<string name="repo_sort_label">Sort:</string>
<string name="repo_sync_all">Sync All</string>
</resources>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import org.wip.plugintoolkit.features.colorpicker.ui.pickers.SimpleRingColorPick
fun ColorPicker(
modifier: Modifier = Modifier,
type: ColorPickerType = ColorPickerType.Classic(),
initialColor: Color = Color.White,
onPickedColor: (Color) -> Unit
) {
Box(modifier = modifier) {
when (type) {
is ColorPickerType.Classic -> ClassicColorPicker(
showAlphaBar = type.showAlphaBar,
initialColor = initialColor,
onPickedColor = onPickedColor,
)

Expand Down Expand Up @@ -62,4 +64,3 @@ fun ColorPicker(
private fun ColorPickerPreview() {
ColorPicker(onPickedColor = {})
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -26,119 +27,103 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import org.jetbrains.compose.resources.stringResource
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.colorpicker.model.ColorPickerType
import org.wip.plugintoolkit.features.colorpicker.utils.toCMYK
import org.wip.plugintoolkit.features.colorpicker.utils.toHSL
import org.wip.plugintoolkit.features.colorpicker.utils.parseHexColor
import org.wip.plugintoolkit.features.colorpicker.utils.toHex
import org.wip.plugintoolkit.features.colorpicker.utils.toRGB
import org.wip.plugintoolkit.features.colorpicker.utils.transparentBackground
import org.wip.plugintoolkit.shared.components.SelectedButtonGroup
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import plugintoolkit.composeapp.generated.resources.Res
import plugintoolkit.composeapp.generated.resources.action_cancel
import plugintoolkit.composeapp.generated.resources.color_picker_apply
import plugintoolkit.composeapp.generated.resources.color_picker_hex
import plugintoolkit.composeapp.generated.resources.color_picker_hex_hint
import plugintoolkit.composeapp.generated.resources.color_picker_title

/**
* Color picker wrapped in a dialog.
*
* @param show Whether the dialog is visible.
* @param onDismissRequest Called when the user tries to dismiss the dialog.
* @param initialType The picker style — defaults to [ColorPickerType.Classic].
* @param onPickedColor Callback invoked when the user confirms a color selection.
*/
/** A focused, editable color picker dialog with explicit cancel/apply actions. */
@Composable
fun ColorPickerDialog(
show: Boolean,
onDismissRequest: () -> Unit,
initialType: ColorPickerType = ColorPickerType.Classic(),
initialColor: Color = Color.White,
showAlpha: Boolean = false,
onPickedColor: (Color) -> Unit
) {
var showDialog by remember(show) { mutableStateOf(show) }
var color by remember { mutableStateOf(Color.White) }
var selectedFormat by remember { mutableStateOf("HEX") }
var type by remember { mutableStateOf(initialType) }
if (!show) return

if (showDialog) {
Dialog(
onDismissRequest = {
onDismissRequest()
showDialog = false
}) {
val includeAlpha = when (type) {
is ColorPickerType.Circle -> (type as ColorPickerType.Circle).showAlphaBar
is ColorPickerType.Classic -> (type as ColorPickerType.Classic).showAlphaBar
is ColorPickerType.Ring -> (type as ColorPickerType.Ring).showAlphaBar
else -> false
}
var color by remember(initialColor) { mutableStateOf(initialColor) }
var hexInput by remember(initialColor, showAlpha) {
mutableStateOf(initialColor.toHex(hexPrefix = true, includeAlpha = showAlpha).uppercase())
}
val parsedHex = remember(hexInput) { parseHexColor(hexInput) }

Dialog(onDismissRequest = onDismissRequest) {
Surface(
modifier = Modifier.widthIn(max = ToolkitTheme.dimensions.minWidthMedium),
shape = MaterialTheme.shapes.extraLarge,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = ToolkitTheme.dimensions.elevationHighMedium
) {
Column(
modifier = Modifier.padding(ToolkitTheme.spacing.extraLarge),
verticalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
) {
Text(
text = stringResource(Res.string.color_picker_title),
style = MaterialTheme.typography.headlineSmall
)

val colorCode = remember(color, selectedFormat) {
when (selectedFormat) {
"HEX" -> color.toHex(hexPrefix = true, includeAlpha = includeAlpha)
"RGB" -> color.toRGB(rgbPrefix = true, includeAlpha = includeAlpha)
"HSL" -> color.toHSL(hslPrefix = true, includeAlpha = includeAlpha)
"CMYK" -> color.toCMYK(cmykPrefix = true, includeAlpha = includeAlpha)
else -> color.toHex(hexPrefix = true, includeAlpha = includeAlpha)
ColorPicker(
type = ColorPickerType.Classic(showAlphaBar = showAlpha),
initialColor = initialColor,
onPickedColor = {
color = it
hexInput = it.toHex(hexPrefix = true, includeAlpha = showAlpha).uppercase()
}
)

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
) {
Box(
modifier = Modifier
.size(ToolkitTheme.dimensions.heightMediumLarge)
.clip(RoundedCornerShape(ToolkitTheme.spacing.small))
.transparentBackground(verticalBoxesAmount = 4)
.background(parsedHex ?: color)
)
OutlinedTextField(
value = hexInput,
onValueChange = { input ->
hexInput = input.take(9)
parseHexColor(hexInput)?.let { color = it }
},
modifier = Modifier.weight(1f),
label = { Text(stringResource(Res.string.color_picker_hex)) },
supportingText = if (parsedHex == null) {
{ Text(stringResource(Res.string.color_picker_hex_hint)) }
} else null,
isError = parsedHex == null,
singleLine = true,
textStyle = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
)
}
}

Surface(
modifier = Modifier.widthIn(max = ToolkitTheme.dimensions.minWidthMedium),
shape = MaterialTheme.shapes.extraLarge,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = ToolkitTheme.dimensions.elevationHighMedium
) {
Box(modifier = Modifier.padding(ToolkitTheme.spacing.extraLarge)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(Res.string.action_cancel))
}
Button(
onClick = { parsedHex?.let(onPickedColor) },
enabled = parsedHex != null
) {
SelectedButtonGroup(
buttons = listOf("HEX", "RGB", "HSL", "CMYK"),
startingIndex = 0,
onButtonSelected = { selectedFormat = it }
)
SelectedButtonGroup(
buttons = listOf("Classic", "Circle", "Ring", "Simple"),
startingIndex = 0,
onButtonSelected = {
type = when (it) {
"Classic" -> ColorPickerType.Classic()
"Circle" -> ColorPickerType.Circle()
"Ring" -> ColorPickerType.Ring()
"Simple" -> ColorPickerType.SimpleRing()
else -> ColorPickerType.Classic()
}
}
)
ColorPicker(type = type, onPickedColor = { color = it })
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
) {
Box(
modifier = Modifier
.size(ToolkitTheme.dimensions.containerWidthMediumLarge, ToolkitTheme.dimensions.heightMediumLarge)
.clip(RoundedCornerShape(50))
.transparentBackground(verticalBoxesAmount = 4)
.background(color)
)
Text(
text = colorCode,
color = MaterialTheme.colorScheme.onSurface,
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
)
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onPickedColor(color)
showDialog = false
},
shape = CircleShape
) {
Text(text = "Select")
}
Text(stringResource(Res.string.color_picker_apply))
}
}
}
Expand All @@ -150,11 +135,6 @@ fun ColorPickerDialog(
@Composable
private fun ColorPickerDialogPreview() {
MaterialTheme {
ColorPickerDialog(
show = true,
onDismissRequest = {},
onPickedColor = {}
)
ColorPickerDialog(show = true, onDismissRequest = {}, onPickedColor = {})
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import org.wip.plugintoolkit.features.colorpicker.utils.fromHueProgress
import org.wip.plugintoolkit.features.colorpicker.utils.green
import org.wip.plugintoolkit.features.colorpicker.utils.lighten
import org.wip.plugintoolkit.features.colorpicker.utils.red
import org.wip.plugintoolkit.features.colorpicker.utils.saturationAndValue
import org.wip.plugintoolkit.features.colorpicker.utils.toHueProgress
import kotlin.math.roundToInt
import org.wip.plugintoolkit.core.theme.ToolkitTheme

Expand All @@ -45,18 +47,33 @@ import org.wip.plugintoolkit.core.theme.ToolkitTheme
internal fun ClassicColorPicker(
modifier: Modifier = Modifier,
showAlphaBar: Boolean,
initialColor: Color = Color.White,
onPickedColor: (Color) -> Unit
) {
var pickerLocation by remember { mutableStateOf(Offset.Zero) }
val initialSaturationAndValue = remember(initialColor) { initialColor.saturationAndValue() }
val initialHue = remember(initialColor) { initialColor.toHueProgress() }
var pickerLocation by remember(initialColor) { mutableStateOf(Offset.Zero) }
var colorPickerSize by remember { mutableStateOf(IntSize.Zero) }
var alpha by remember { mutableStateOf(1f) }
var rangeColor by remember { mutableStateOf(Color.White) }
var hueSlider by remember { mutableStateOf(0f) }
var pickerInitialized by remember(initialColor) { mutableStateOf(false) }
var alpha by remember(initialColor) { mutableStateOf(initialColor.alpha) }
var rangeColor by remember(initialColor) { mutableStateOf(Color.fromHueProgress(initialHue)) }
var hueSlider by remember(initialColor) { mutableStateOf(initialHue) }

var color by remember { mutableStateOf(Color.White) }
var color by remember(initialColor) { mutableStateOf(initialColor) }

LaunchedEffect(rangeColor, pickerLocation, colorPickerSize, alpha) {
if (colorPickerSize.width > 0 && colorPickerSize.height > 0) {
LaunchedEffect(colorPickerSize, initialColor) {
if (colorPickerSize.width > 0 && colorPickerSize.height > 0 && !pickerInitialized) {
val (saturation, value) = initialSaturationAndValue
pickerLocation = Offset(
x = saturation * colorPickerSize.width,
y = (1f - value) * colorPickerSize.height
)
pickerInitialized = true
}
}

LaunchedEffect(rangeColor, pickerLocation, colorPickerSize, alpha, pickerInitialized) {
if (pickerInitialized && colorPickerSize.width > 0 && colorPickerSize.height > 0) {
val xProgress = if (colorPickerSize.width > 0) {
(1 - (pickerLocation.x / colorPickerSize.width)).coerceIn(0f, 1f)
} else 0f
Expand Down Expand Up @@ -142,4 +159,3 @@ private fun ClassicColorPickerPreview() {
ClassicColorPicker(showAlphaBar = true, onPickedColor = {})
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,37 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

/** Parses #RGBA, #RRGGBB or #AARRGGBB; long alpha values match the ARGB order emitted by [toHex]. */
fun parseHexColor(value: String): Color? {
val digits = value.trim().removePrefix("#")
val normalized = when (digits.length) {
4 -> {
val red = digits[0].toString().repeat(2)
val green = digits[1].toString().repeat(2)
val blue = digits[2].toString().repeat(2)
val alpha = digits[3].toString().repeat(2)
"$alpha$red$green$blue"
}
6 -> "FF$digits"
8 -> digits
else -> return null
}
val argb = normalized.toLongOrNull(16) ?: return null
return Color(argb.toInt())
}

/** Detects alpha-bearing legacy and current color representations without changing their format. */
fun colorStringHasAlpha(value: String): Boolean {
val trimmed = value.trim()
if (trimmed.startsWith("rgba(", ignoreCase = true) || trimmed.startsWith("hsla(", ignoreCase = true)) {
return true
}
val candidate = value.split(",").lastOrNull { it.trim().isNotEmpty() }?.trim().orEmpty()
val digits = candidate.removePrefix("#")
return (candidate.startsWith("#") && digits.length == 4 && digits.toLongOrNull(16) != null) ||
(digits.length == 8 && digits.toLongOrNull(16) != null)
}

/**
* Returns an integer array for all color channels value.
*/
Expand Down Expand Up @@ -236,5 +267,15 @@ internal fun Color.toHueProgress(): Float {
hue *= 60
if (hue < 0) hue += 360

return hue
return hue / 360f
}

internal fun Color.saturationAndValue(): Pair<Float, Float> {
val red = red() / 255f
val green = green() / 255f
val blue = blue() / 255f
val maximum = max(red, max(green, blue))
val minimum = min(red, min(green, blue))
val saturation = if (maximum == 0f) 0f else (maximum - minimum) / maximum
return saturation to maximum
}
Loading