Code review fixes

This commit is contained in:
Keavon Chambers
2026-05-17 15:44:02 -04:00
parent 1d3d32c169
commit 5faf5e67f2
9 changed files with 148 additions and 49 deletions

View File

@@ -31,10 +31,15 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
SaveFileDialogContext::File { content } => {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
}
SaveFileDialogContext::MultipleFiles { files } => {
// Treat the chosen path as the folder name; strip any extension the user typed (e.g. "MyAnim.png" → "MyAnim").
SaveFileDialogContext::MultipleFiles { files, expected_extension } => {
// Treat the chosen path as the folder name. Strip only the export's expected extension (e.g. ".png" for a
// PNG animation export) so that arbitrary dotted folder names like `v1.0` are preserved as-is, while a user
// who typed `MyAnim.png` still gets a `MyAnim/` folder rather than a `MyAnim.png/` folder.
// The `WriteFile` handler creates parent directories if they don't exist, so the folder is materialized on first write.
let folder = path.with_extension("");
let folder = match path.extension().and_then(|e| e.to_str()) {
Some(ext) if ext.eq_ignore_ascii_case(&expected_extension) => path.with_extension(""),
_ => path,
};
for (filename, content) in files {
let file_path = folder.join(&filename);
dispatcher.respond(DesktopFrontendMessage::WriteFile { path: file_path, content });

View File

@@ -71,10 +71,16 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
// Materialize each frame to bytes; SVG strings are encoded as UTF-8.
// Raster-needs-canvas-rasterize frames can't be encoded here without a Rust SVG rasterizer,
// so fall through to the frontend zip path in that case.
// TODO: This fallback is inconsistent with the desktop folder-save flow for the rest of the animation
// export — desktop users will see a .zip download instead of a folder. Once SVG rasterization moves to
// Rust (resvg), this fallback can be removed and all frame paths can save into the chosen folder.
let mut needs_frontend_rasterization = false;
let mut materialized = Vec::with_capacity(frames.len());
// Dynamic zero-pad width so files keep sorting in playback order beyond 9,999 frames.
let pad_width = frames.len().to_string().len().max(4);
let safe_base = graphite_editor::messages::frontend::utility_types::sanitize_filename_component(&name);
for (index, frame) in frames.iter().enumerate() {
let filename = format!("{name}_{:04}.{extension}", index + 1);
let filename = format!("{safe_base}_{:0pad$}.{extension}", index + 1, pad = pad_width);
let bytes = match frame {
ExportAnimationFrame::Svg(svg) if extension == "svg" => svg.as_bytes().to_vec(),
ExportAnimationFrame::Bytes(bytes) => bytes.to_vec(),
@@ -100,10 +106,13 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
// The dialog name is the folder the frames go into (analogous to the .zip on web).
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save Animation Frames Folder As".to_string(),
default_filename: name.clone(),
default_filename: safe_base,
default_folder: folder,
filters: Vec::new(),
context: SaveFileDialogContext::MultipleFiles { files: materialized },
context: SaveFileDialogContext::MultipleFiles {
files: materialized,
expected_extension: extension,
},
});
}
FrontendMessage::TriggerVisitLink { url } => {

View File

@@ -118,11 +118,13 @@ pub enum SaveFileDialogContext {
File {
content: Vec<u8>,
},
/// Multiple files written into a folder whose path is the user-chosen path with any extension stripped
/// (e.g. picking `MyAnim.png` yields a `MyAnim/` folder). Each `(filename, content)` entry is written
/// Multiple files written into a folder whose path is the user-chosen path with the matching `expected_extension`
/// stripped (e.g. for a PNG animation export, picking `MyAnim.png` yields a `MyAnim/` folder, while picking `v1.0`
/// is preserved as `v1.0/` because `.0` isn't the expected extension). Each `(filename, content)` entry is written
/// inside that folder, which is created if it doesn't exist.
MultipleFiles {
files: Vec<(String, Vec<u8>)>,
expected_extension: String,
},
}