fix: prevent custom action state images and text from being overriden by plugin events

This commit is contained in:
nekename authored and GitHub committed 2026-10-18 18:56:20 +01:00
1 parent 5de234fd2b
commit 772189ca74
9 files changed
+82 -41

No files matched your search

+8 -8
View File
@@ -61,17 +61,17 @@ fn get_encoder_image(encoder: &mut Encoder, instance: &ActionInstance) -> Result
// Override the title if it's set in the state.
let override_title = {
let t = instance.states[state_idx].text.trim();
(!t.is_empty()).then(|| t.to_string())
let t = instance.states[state_idx]
.text_override
.clone()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| instance.states[state_idx].text.clone());
(!t.is_empty()).then_some(t)
};
// Similar to the title, except for the rendered icon (we check whether the state matches the default, and if it doesn't, we use it).
// Similar to the title, except for the rendered icon.
let state_image = &instance.states[state_idx].image.trim();
let override_icon = {
let default_image = &instance.action.states[state_idx].image.trim();
(!state_image.is_empty() && state_image != default_image).then(|| state_image.to_string())
};
let override_icon = instance.states[state_idx].image_override.clone();
// We need to do small item corrections here.
let mut feedback = Map::new();
@@ -130,6 +130,13 @@ pub async fn move_instance(source: Context, destination: Context, retain: bool)
if path.starts_with(&old_dir) {
state.image = new_dir.join(path.strip_prefix(&old_dir).unwrap()).to_string_lossy().into_owned();
}
if let Some(image_override) = state.image_override.as_mut() {
let override_path = std::path::Path::new(image_override);
if override_path.starts_with(&old_dir) {
*image_override = new_dir.join(override_path.strip_prefix(&old_dir).unwrap()).to_string_lossy().into_owned();
}
}
}
let dst = get_slot_mut(&destination, &mut locks).await?;
+1 -1
View File
@@ -56,7 +56,7 @@ pub async fn title_parameters_did_change(instance: &ActionInstance, state: u16)
column: instance.context.position % 3,
},
state: instance.current_state,
title: state.text,
title: state.text_override.filter(|s| !s.is_empty()).unwrap_or(state.text),
titleParameters: TitleParameters {
fontFamily: String::new(),
fontSize: state.size.0,
+8
View File
@@ -128,6 +128,9 @@ pub struct ActionState {
#[serde(alias = "Image")]
pub image: String,
// Note: this is not a real manifest property; it is only used internally.
#[serde(alias = "ImageOverride")]
pub image_override: Option<String>,
// Note: this is not a real manifest property; it is only used internally.
#[serde(alias = "ImageScale")]
pub image_scale: u8,
// Note: this is not a real manifest property; it is only used internally.
@@ -137,6 +140,9 @@ pub struct ActionState {
pub name: String,
#[serde(alias = "Title")]
pub text: String,
// Note: this is not a real manifest property; it is only used internally.
#[serde(alias = "TitleOverride")]
pub text_override: Option<String>,
#[serde(alias = "ShowTitle")]
pub show: bool,
#[serde(alias = "TitleColor")]
@@ -163,10 +169,12 @@ impl Default for ActionState {
fn default() -> Self {
Self {
image: "actionDefaultImage".to_owned(),
image_override: None,
image_scale: 100,
background_colour: "#000000".to_owned(),
name: String::new(),
text: String::new(),
text_override: None,
show: true,
colour: "#FFFFFF".to_owned(),
stroke_colour: "#000000".to_owned(),
+37 -19
View File
@@ -87,13 +87,13 @@ impl From<ActionInstance> for DiskActionInstance {
}
};
for (index, state) in value.states.iter_mut().enumerate() {
if state.image.trim() == "data:" {
state.image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg==".to_owned();
let normalise_image = |index: usize, image: &mut String, suffix: &str| {
if image.trim() == "data:" {
*image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg==".to_owned();
}
if state.image.starts_with("data:") && state.image.contains('/') && state.image.contains(',') {
let mut extension = state.image.split_once('/').unwrap().1.split_once(',').unwrap().0;
if image.starts_with("data:") && image.contains('/') && image.contains(',') {
let mut extension = image.split_once('/').unwrap().1.split_once(',').unwrap().0;
if extension.contains(';') {
extension = extension.split_once(';').unwrap().0;
}
@@ -101,24 +101,35 @@ impl From<ActionInstance> for DiskActionInstance {
extension = extension.split_once('+').unwrap().0;
}
let data = if state.image.contains(";base64,") {
let data = if image.contains(";base64,") {
use base64::Engine;
let Ok(data) = base64::engine::general_purpose::STANDARD.decode(state.image.split_once(";base64,").unwrap().1) else {
continue;
let Ok(data) = base64::engine::general_purpose::STANDARD.decode(image.split_once(";base64,").unwrap().1) else {
return;
};
data
} else {
state.image.split_once(',').unwrap().1.as_bytes().to_vec()
image.split_once(',').unwrap().1.as_bytes().to_vec()
};
let filename = format!("{}.{}", index, extension);
let filename = format!("{}{}.{}", index, suffix, extension);
if fs::create_dir_all(&image_dir).is_err() || fs::write(image_dir.join(&filename), data).is_err() {
continue;
return;
};
state.image = filename;
*image = filename;
}
state.image = normalise_path(&state.image);
*image = normalise_path(image);
};
for (index, state) in value.states.iter_mut().enumerate() {
if state.text_override.as_ref().map(|s| s.is_empty()).unwrap_or(false) {
state.text_override = None;
}
if let Some(ref mut image) = state.image_override {
normalise_image(index, image, "-user");
}
normalise_image(index, &mut state.image, "");
}
for state in value.action.states.iter_mut() {
state.image = normalise_path(&state.image);
@@ -153,20 +164,27 @@ impl DiskActionInstance {
}
};
let mut states = self.states.clone();
for state in states.iter_mut() {
if let Some(true) = state.image.chars().next().map(|v| v.is_numeric()) {
state.image = config_dir
let reconstruct_image = |image: &mut String| {
if let Some(true) = image.chars().next().map(|v| v.is_numeric()) {
*image = config_dir
.join("images")
.join(&device)
.join(&profile)
.join(self.context.to_string())
.join(&state.image)
.join(&image)
.to_string_lossy()
.into_owned();
} else {
state.image = reconstruct_path(&state.image);
*image = reconstruct_path(image);
}
};
let mut states = self.states.clone();
for state in states.iter_mut() {
if let Some(ref mut image) = state.image_override {
reconstruct_image(image);
}
reconstruct_image(&mut state.image);
}
let mut action = self.action.clone();
for state in action.states.iter_mut() {
+11 -8
View File
@@ -39,8 +39,8 @@
let result = reader.result?.toString();
if (result) {
let resized = await resizeImage(result);
if (resized) instance.states[state].image = resized;
else instance.states[state].image = result;
if (resized) instance.states[state].image_override = resized;
else instance.states[state].image_override = result;
}
};
@@ -89,7 +89,10 @@
on:drop={handleDrop}
on:contextmenu={(event) => {
event.preventDefault();
instance.states[state].image = instance.action.states[state]?.image ?? instance.action.icon;
if (instance.states[state].image_override) instance.states[state].image_override = null;
// Support further resetting the base image to the manifest value as a migration path
// for profiles where the base image and user override image were indistinguishable.
else instance.states[state].image = instance.action.states[state]?.image ?? instance.action.icon;
}}
title={$t("instance_editor.image.hint")}
aria-label={$t("instance_editor.image.hint")}
@@ -173,7 +176,7 @@
if (!context) return;
context.fillStyle = solidColourInput.value;
context.fillRect(0, 0, canvas.width, canvas.height);
instance.states[state].image = canvas.toDataURL("image/png");
instance.states[state].image_override = canvas.toDataURL("image/png");
}}
/>
</button>
@@ -191,8 +194,8 @@
let result = reader.result?.toString();
if (result) {
let resized = await resizeImage(result);
if (resized) instance.states[state].image = resized;
else instance.states[state].image = result;
if (resized) instance.states[state].image_override = resized;
else instance.states[state].image_override = result;
}
};
@@ -204,8 +207,8 @@
<div class="flex flex-row items-center space-x-2">
<label for="editor-text">{$t("instance_editor.text")}</label>
<textarea
bind:value={instance.states[state].text}
placeholder={instance.action.states[state]?.text || instance.action.name}
bind:value={instance.states[state].text_override}
placeholder={instance.states[state].text}
rows="1"
class="w-full px-1 text-neutral-300 bg-neutral-600 border border-neutral-500 rounded-lg resize-none"
id="editor-text"
+2 -1
View File
@@ -188,7 +188,8 @@
await invoke("trigger_virtual_press", { context });
}
$: accessibleLabel = label + (slot ? ": " + slot.action.name + (state?.show && state?.text ? " - " + state.text : "") : "");
$: accessibleLabel =
label + (slot ? ": " + slot.action.name + (state?.show && (state?.text_override || state?.text) ? " - " + (state.text_override || state.text) : "") : "");
</script>
<div class="relative" style={`transform: scale(${(112 /* desired inner size */ / size) * scale});`}>
+2
View File
@@ -1,9 +1,11 @@
export type ActionState = {
image: string;
image_override: string | null;
image_scale: number;
background_colour: string;
name: string;
text: string;
text_override: string | null;
show: boolean;
colour: string;
stroke_colour: string;
+6 -4
View File
@@ -76,7 +76,8 @@ export async function renderImage(
// Load image
const image = document.createElement("img");
image.crossOrigin = "anonymous";
image.src = processImage ? getImage(state.image, fallback) : state.image;
const imageSource = state.image_override ?? state.image;
image.src = processImage ? getImage(imageSource, fallback) : imageSource;
if (image.src == undefined) return;
await new Promise((resolve, reject) => {
image.onload = resolve;
@@ -107,6 +108,7 @@ export async function renderImage(
// Draw text
if (state.show) {
const textSource = state.text_override || state.text;
const size = state.size * 2 * scale;
context.textAlign = "center";
context.font =
@@ -116,16 +118,16 @@ export async function renderImage(
context.lineWidth = state.stroke_size * scale;
context.textBaseline = "top";
const x = canvas.width / 2;
let y = canvas.height / 2 - size * state.text.split("\n").length * 0.5;
let y = canvas.height / 2 - size * textSource.split("\n").length * 0.5;
switch (state.alignment) {
case "top":
y = context.lineWidth;
break;
case "bottom":
y = canvas.height - size * state.text.split("\n").length - context.lineWidth;
y = canvas.height - size * textSource.split("\n").length - context.lineWidth;
break;
}
for (const [index, line] of Object.entries(state.text.split("\n"))) {
for (const [index, line] of Object.entries(textSource.split("\n"))) {
context.strokeText(line, x, y + size * parseInt(index));
context.fillText(line, x, y + size * parseInt(index));
if (state.underline) {