Web: open, import, save and export files in the browser (fixes #63, fixes #64)

The web build could not reach any file: File > Open Session…, Import > Audio… and Import > MIDI…
showed a text box for a path the browser can't read, and every save or export failed with
«operation not supported on this platform», so work was lost on close.

On the web now:
- Open Session…, Import Audio… and Import MIDI… show the browser's file picker; files dropped on
  the page open (sessions) or import (audio, MIDI) by extension. Files travel as bytes; imports are
  one undo step each, like the import commands.
- A session is saved as ONE file, a bundle `<name>.scraftz`: a zip (stored entries) holding the
  `.scraft` and its `Audio Files/` as 32-bit float WAV — the same layout as a session folder, since
  the browser has no folders. `io::open_session_bytes` opens a bundle or a bare `.scraft` (its audio
  is then reported missing). Bundles are hostile input: entry count and size are capped, and
  declared sizes are never read past.
- Every single-file output (save, Save As Template, bounce, MIDI/score/text export, track presets)
  goes through `io::write_file`: a file natively, a download in the browser via a delivery hook
  (`io::set_deliver`) that only the web app installs.
- In the browser Save As asks for the bundle's name (`<name>.scraftz`) instead of a folder path.
Native builds keep writing files and session folders as before. Folder outputs (stems, clip
exports) are still not available on the web.

Pairs with #157 (Save on a never-saved session opens Save As), which the browser needs too: the
web app always starts on the unsaved demo.

New dependency: `zip` (MIT), default features off (stored entries only).

Checked: engine tests for the bundle round trip (every sample identical), same-named sources,
bare sessions and hostile bundles; `cargo xtask ci`. In Chromium on the packaged web build:
⌘⇧I picks and imports a WAV; WAV, MP3, FLAC, OGG, M4A, AIFF and MIDI import by drop; Save As →
`Midnight Groove.scraftz` downloads with the imported audio inside; that bundle reopens in a fresh
page with all tracks and no missing media, and saves again with the same content.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
natocool2
2026-10-10 19:17:07 +01:00
co-authored by Claude Opus 5.5
parent 330a698397
commit 6964c50d00
10 changed files with 484 additions and 24 deletions
Generated
+36
View File
@@ -2133,6 +2133,18 @@ dependencies = [
"jni-sys 0.3.1",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
@@ -3502,6 +3514,7 @@ dependencies = [
"soundcraft-video",
"soundcraft-vst3-host",
"thiserror 2.0.21",
"zip",
]
[[package]]
@@ -3623,8 +3636,12 @@ version = "0.4.0"
dependencies = [
"eframe",
"egui",
"js-sys",
"log",
"rfd",
"serde_json",
"soundcraft-engine",
"soundcraft-model",
"soundcraft-playback",
"soundcraft-ui-egui",
"wasm-bindgen",
@@ -4084,6 +4101,12 @@ dependencies = [
"rustc-hash 2.1.3",
]
[[package]]
name = "typed-path"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
[[package]]
name = "uds_windows"
version = "1.2.1"
@@ -5374,6 +5397,19 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "zip"
version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
dependencies = [
"crc32fast",
"indexmap",
"memchr",
"nix",
"typed-path",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"