forked from 1b-agent/opencadstudio-upstream
feat: in-place save (File System Access), image paste, print wiring, web capture
The last high-severity web gap plus a small integration bundle.
In-place save: with the File System Access API (Chromium/Edge), Save
now works like desktop. Plain Save on a tab with a retained handle
writes straight through — no picker, no download, no "(1)" copies.
First save or format change opens showSaveFilePicker AT the confirm
gesture (the worker serialization that follows can outlast the
transient-activation window; retained handles need no activation to
write). Picker cancel cancels the save like desktop; picker or write
failure falls back to the legacy download with a note; Firefox/Safari
keep the download flow untouched (feature-detected). Handles are
keyed by tab id in a thread-local registry (Message must be Send —
JsValue cannot ride in messages or App state); closing a tab drops
its handle; Save As re-picks. Dirty clears only on successful
persistence, same race guard as before.
Image paste: Ctrl+V of a screenshot inserts it as an image reference
— the clipboard reader falls back to image/* items when neither the
entity clipboard nor text has anything (precedence preserved), the
bytes enter the image source registry as pasted-<timestamp>.<ext>,
and attach reuses the IMAGE command's model-creation path verbatim.
Browser print: beforeprint snapshots the WebGL canvas into a print-
only <img> (the capture_canvas technique), so the browser's own print
menu produces the drawing instead of a blank page.
Automation capture on web: the capture op no longer calls the
synchronous GPU-map screenshot (dead on wasm); main's frame-parked
capture waits for a presented frame and reads via capture_canvas — the
same workaround the save-thumbnail path uses — delivering the PNG as a
download.
(cherry picked from commit 6e89b36d)
Ported onto main (#1717 review): the web capture rides main's existing
frame-parked capture (one ControlCaptureFrame path, ignoring the timer
tick on wasm) instead of a second parked-capture state; closing a tab
drops its file handle inside main's on_tab_close.
This commit is contained in:
1 parent
0459f20129
commit
835e8d469f
33 files changed
+1084
-29
No files matched your search
+92
@@ -311,6 +311,98 @@ pub async fn read_clipboard_text() -> Option<String> {
|
||||
value.as_string()
|
||||
}
|
||||
|
||||
/// Extension the pasted-image registry name carries for a clipboard MIME
|
||||
/// type. Unknown image types fall back to `png` — the flavor every OS
|
||||
/// screenshot paste uses — so the reference still names a decodable format.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // tested everywhere, used on wasm
|
||||
pub(crate) fn clipboard_image_extension(mime_type: &str) -> &'static str {
|
||||
match mime_type.trim().to_ascii_lowercase().as_str() {
|
||||
"image/jpeg" | "image/jpg" => "jpg",
|
||||
"image/webp" => "webp",
|
||||
"image/gif" => "gif",
|
||||
"image/bmp" | "image/x-ms-bmp" | "image/x-bmp" => "bmp",
|
||||
_ => "png",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_image_extensions_cover_the_paste_flavors() {
|
||||
assert_eq!(clipboard_image_extension("image/png"), "png");
|
||||
assert_eq!(clipboard_image_extension("IMAGE/JPEG"), "jpg");
|
||||
assert_eq!(clipboard_image_extension(" image/webp "), "webp");
|
||||
assert_eq!(clipboard_image_extension("image/gif"), "gif");
|
||||
assert_eq!(clipboard_image_extension("image/x-ms-bmp"), "bmp");
|
||||
// Anything else (a vendor flavor, a bare "image/*") still gets a name
|
||||
// the image registry can key and the `image` crate can decode.
|
||||
assert_eq!(clipboard_image_extension("image/avif"), "png");
|
||||
assert_eq!(clipboard_image_extension(""), "png");
|
||||
}
|
||||
|
||||
/// Web: read an image from the system clipboard via the async Clipboard API
|
||||
/// (`navigator.clipboard.read()`), used when a paste found no text. Returns
|
||||
/// the registry name (`pasted-YYYYMMDD-HHMMSS.<ext>`) plus the image bytes,
|
||||
/// or `None` when the clipboard holds no image item or the read is denied.
|
||||
/// Like `read_clipboard_text`, the Ctrl+V keypress that reaches it is a user
|
||||
/// gesture, which is what makes the permission-gated read allowed at all.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn read_clipboard_image() -> Option<(String, Vec<u8>)> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let clipboard = web_sys::window()?.navigator().clipboard();
|
||||
let items = wasm_bindgen_futures::JsFuture::from(clipboard.read())
|
||||
.await
|
||||
.ok()?;
|
||||
let items = js_sys::Array::from(&items);
|
||||
for index in 0..items.length() {
|
||||
let Ok(item) = items.get(index).dyn_into::<web_sys::ClipboardItem>() else {
|
||||
continue;
|
||||
};
|
||||
// A clipboard item can offer several flavors; take the first image
|
||||
// one (browsers list at most one per item in practice). An item with
|
||||
// no image flavor just moves the search to the next one.
|
||||
let Some(mime_type) = item
|
||||
.types()
|
||||
.iter()
|
||||
.filter_map(|kind| kind.as_string())
|
||||
.find(|mime| mime.starts_with("image/"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let blob = wasm_bindgen_futures::JsFuture::from(item.get_type(&mime_type))
|
||||
.await
|
||||
.ok()?
|
||||
.dyn_into::<web_sys::Blob>()
|
||||
.ok()?;
|
||||
let buffer = wasm_bindgen_futures::JsFuture::from(blob.array_buffer())
|
||||
.await
|
||||
.ok()?;
|
||||
let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
return Some((pasted_image_name(&mime_type), bytes));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Registry name for a pasted clipboard image: `pasted-YYYYMMDD-HHMMSS.ext`
|
||||
/// stamped from the wall clock (local time, the same clock a screenshot file
|
||||
/// name uses) with the extension derived from the MIME type.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn pasted_image_name(mime_type: &str) -> String {
|
||||
let now = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(0.0));
|
||||
format!(
|
||||
"pasted-{:04}{:02}{:02}-{:02}{:02}{:02}.{}",
|
||||
now.get_full_year(),
|
||||
now.get_month() + 1,
|
||||
now.get_date(),
|
||||
now.get_hours(),
|
||||
now.get_minutes(),
|
||||
now.get_seconds(),
|
||||
clipboard_image_extension(mime_type)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(module = "/web/clipboard.js")]
|
||||
extern "C" {
|
||||
|
||||
Reference in new issue
Block a user