diff --git a/src-tauri/src/encoder_layouts.rs b/src-tauri/src/encoder_layouts.rs index 575998f..2a93eab 100644 --- a/src-tauri/src/encoder_layouts.rs +++ b/src-tauri/src/encoder_layouts.rs @@ -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(); diff --git a/src-tauri/src/events/frontend/instances.rs b/src-tauri/src/events/frontend/instances.rs index 25ecafc..7e9ee85 100644 --- a/src-tauri/src/events/frontend/instances.rs +++ b/src-tauri/src/events/frontend/instances.rs @@ -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?; diff --git a/src-tauri/src/events/outbound/states.rs b/src-tauri/src/events/outbound/states.rs index 4355358..cca76d0 100644 --- a/src-tauri/src/events/outbound/states.rs +++ b/src-tauri/src/events/outbound/states.rs @@ -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, diff --git a/src-tauri/src/shared.rs b/src-tauri/src/shared.rs index 418ceb6..905bbd5 100644 --- a/src-tauri/src/shared.rs +++ b/src-tauri/src/shared.rs @@ -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, + // 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, #[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(), diff --git a/src-tauri/src/store/simplified_profile.rs b/src-tauri/src/store/simplified_profile.rs index d1926b7..8e35d18 100644 --- a/src-tauri/src/store/simplified_profile.rs +++ b/src-tauri/src/store/simplified_profile.rs @@ -87,13 +87,13 @@ impl From 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 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() { diff --git a/src/components/InstanceEditor.svelte b/src/components/InstanceEditor.svelte index 1ce9c12..6dc8586 100644 --- a/src/components/InstanceEditor.svelte +++ b/src/components/InstanceEditor.svelte @@ -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"); }} /> @@ -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 @@