Skip to content

Presets: import and export, and three filename bugs (#81) - #96

Merged
StuartCameronCode merged 1 commit into
mainfrom
feat/preset-import-export
Sep 12, 2026
Merged

StuartCameronCode merged 1 commit into
mainfrom
feat/preset-import-export

Conversation

@StuartCameronCode

Copy link
Copy Markdown
Owner

Closes issue #81's fourth ask — "consider JSON profiles as implemented in Massie VFX Open Media Encoder".

The format already existed and is richer than what was asked for: a preset is JSON carrying the whole pipeline and the encoding settings, in plain files under ~/.vapourbox/presets. What was missing is everything around it. So this is an import/export and error-surfacing change wearing a format request's clothes, which is what makes it small.

Import is two-phase, and that is the substance

Verified by running it: a preset carries customVapoursynthPython the worker executes — and customFfmpegArgs, both surviving a round trip intact. Importing someone else's preset is therefore closer to running their script than to loading their settings, and nothing else in the app would reveal it: those fields only render in advanced mode.

So inspectPresetFile validates without committing, the confirmation dialog shows any custom code verbatim, and commitImport takes stripCustomCode — surfaced as an "Import without the code" button, the middle option between trusting everything and importing nothing.

Import deliberately does not apply the preset. "Add this to my presets" should not replace the user's current pipeline as a side effect, and there is no undo.

Three bugs found while writing it

All pre-existing; all become far easier to hit with import, because an imported preset is very likely to be named like one the user already has. Each has a regression test named after the behaviour, and each was reproduced before being changed.

  • The on-disk filename came from the preset's name. The sanitizer lowercases and collapses whitespace and separators, so VHS Cleanup, vhs cleanup, VHS/Cleanup and VHS Cleanup all produced vhs_cleanup.json — saving one destroyed another.
  • Renaming wrote a second file and left the original, so the preset appeared twice after a reload.
  • deletePreset removed the name-derived file before looking by id, so deleting one preset took a similarly-named one's file with it.

Filenames are the preset id now; savePreset sweeps any other file carrying the same id, which migrates legacy name-based files as they are touched.

Also

  • isBuiltIn is never taken from a file. The flag survives fromJson, and a built-in cannot be deleted or overwritten — a preset claiming to be one would be stuck in the menu permanently.
  • Load failures are collected, not printed. A print() per bad file means that in a release build the preset simply never appears, indistinguishable from one that was never saved. They now surface as a red row in the preset menu with the filename, the reason in plain words, and the full path.
  • saveAsPreset never called notifyListeners(), so a newly-saved preset only appeared once something unrelated rebuilt the toolbar.

Tests

app/test/preset_service_test.dart is the first test over this service at all, which is how the three bugs survived. 25 tests; directoryOverride exists so they run against a temp directory rather than the real $HOME. Push gate 877 locally (was 852).

Verified by hand in a packaged build against five sample files: ordinary, carries-custom-code, malformed, valid-JSON-but-not-a-preset, and one claiming to be built-in.

Not done

Encoder-only profiles. A preset always carries the whole pipeline, so "just my ProRes output settings" cannot be expressed without also pinning someone's deinterlacer. That needs an optional-pipeline model change and is its own piece of work. Noted in CLAUDE.md.

Note on CI

The PR's own checks will fail at "Download dependencies" on all four platforms — deps-version.json names deps-v1.10.0, which is still a draft, and CI's read-only token cannot see draft assets. That affects every PR in the repo and main itself, not this branch. A gate run dispatched with deps_run_id is the real signal; see the comment below.

🤖 Generated with Claude Code

Issue #81's fourth ask was for "JSON profiles as implemented in Massie VFX
Open Media Encoder". The format already existed and is richer than what
was asked for — a preset is JSON carrying the whole pipeline *and* the
encoding settings, in plain files under ~/.vapourbox/presets. What was
missing is everything around it, so this is an import/export and
error-surfacing change wearing a format request's clothes.

Import is two-phase, and that is the substance of the change rather than a
structural nicety. A preset carries `customVapoursynth` — Python the
worker executes — and `customFfmpegArgs`, both verified to survive a round
trip. Importing someone else's preset is therefore closer to running their
script than to loading their settings, and nothing else in the app would
reveal it: those fields only render in advanced mode. So
`inspectPresetFile` validates without committing, the dialog shows any
custom code verbatim, and `commitImport` takes `stripCustomCode` — the
middle option between trusting everything and importing nothing.

Import deliberately does not apply the preset. "Add this to my presets"
should not replace the user's current pipeline as a side effect, and there
is no undo.

Three bugs found while writing it, each with a regression test named after
the behaviour. All are pre-existing and all become far easier to hit with
import, because an imported preset is very likely to be named like one the
user already has:

- The on-disk filename came from the preset's *name*, and the sanitizer
  lowercases and collapses whitespace and separators — so "VHS Cleanup",
  "vhs cleanup", "VHS/Cleanup" and "VHS  Cleanup" all produced
  vhs_cleanup.json. Saving one destroyed another. Filenames are now the
  preset id; savePreset sweeps any other file carrying the same id, which
  migrates legacy files as they are touched.
- Renaming a preset wrote a second file and left the original, so it
  appeared twice after a reload.
- deletePreset removed the name-derived file *before* looking by id, so
  deleting one preset took a similarly-named one's file with it.

`isBuiltIn` is never taken from a file. The flag survives fromJson, and a
built-in cannot be deleted or overwritten, so a preset claiming to be one
would be stuck in the menu permanently.

Load failures are collected instead of printed. A `print()` per bad file
means that in a release build the preset simply never appears, which is
indistinguishable from one that was never saved — tolerable while the app
wrote every preset itself, wrong once files arrive from elsewhere. They
now surface as a red row in the preset menu with the filename, the reason
in plain words, and the full path.

Also fixes `saveAsPreset` never calling notifyListeners(), so a
newly-saved preset only appeared once something unrelated rebuilt the
toolbar.

app/test/preset_service_test.dart is the first test over this service at
all, which is how the three bugs survived; `directoryOverride` exists so
it can run against a temp directory rather than the real $HOME.

Not done: encoder-only profiles. A preset always carries the whole
pipeline, so "just my ProRes output settings" cannot be expressed without
also pinning someone's deinterlacer. That needs an optional-pipeline model
change and is its own piece of work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@StuartCameronCode

Copy link
Copy Markdown
Owner Author

Push gate: green on all four platformsrun 34682762362

platform result
Windows
Linux
macOS x64
macOS arm64

Dispatched with deps_run_id pointing at the build-deps-* runs that produced the 1.10.0 bundles, because the PR's own check cannot pass while deps-version.json names a draft tag.

All 25 new preset_service_test.dart cases ran rather than being skipped — confirmed in the log, including the three regression tests:

saving names the file after the id, not the name
saving renaming does not leave the old file behind
saving two names that sanitize alike do not overwrite each other
deleting removes only the named preset
load failures are reported, not swallowed ...

The red check above is not this branch. Every job dies at "Download dependencies" with release not found: deps-v1.10.0 is still a draft and CI's read-only token cannot see draft assets. It affects every PR in the repo and main itself — main's own CI has been red since 2f6256d on 31 Aug. Publishing that deps release (gh release edit deps-v1.10.0 --draft=false --latest=false) fixes it everywhere at once.

@StuartCameronCode
StuartCameronCode merged commit 9ac2583 into main Sep 12, 2026
8 checks passed
StuartCameronCode added a commit that referenced this pull request Sep 12, 2026
Presets: import and export, and three filename bugs (#81)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant