Skip to content

Diamond File Converter 2.0 - #3

Draft
WillTDA wants to merge 90 commits into
masterfrom
2.0.0
Draft

WillTDA wants to merge 90 commits into
masterfrom
2.0.0

Conversation

@WillTDA

@WillTDA WillTDA commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Diamond File Converter 2.0: a rebuilt conversion architecture with a context-isolated renderer, a job card grid replacing the old queue list, batch and folder ingest, frame trimming, clip joining, and a run of manual QA fixes.

  • New 2.0 conversion architecture, renderer moved behind a context-isolated bridge with a token layer
  • Job card grid replaces the queue list, with checkbox selection and bulk edit
  • Folder ingest with per-type selection and skip reasons
  • Frame range selection for conversions
  • Join clips into one file from the rail
  • Explorer integration, ffprobe packaging, dependency updates (Electron 41 to 44)
  • Numerous fixes from manual QA passes across alpha and beta builds

Test plan

  • Follow the steps in docs/MANUAL-TESTING.md
  • Verify drag-and-drop and folder ingest
  • Verify conversion, frame trimming, and clip joining end to end
  • Verify Explorer context menu integration
  • Confirm installer builds and updates behave as expected for a pre-release

WillTDA added 30 commits August 27, 2026 19:37
Bumps to 2.0.0-alpha.1 and brings the build config in line with the other
Diamond apps.

- Add installer.nsh registering a 'Convert with Diamond File Converter'
  Explorer entry for both files and directories, plus fileAssociations for
  all 21 supported extensions. File Converter was the only one of the three
  apps with neither, so it had no shell integration at all.
- Bundle ffprobe-static alongside ffmpeg. ffmpeg-static ships only ffmpeg.exe,
  so no metadata was previously available anywhere in the app.
- Drop fluent-ffmpeg. Pipelines need filter_complex and explicit -map, which
  it obscures, and -progress pipe:1 reports more reliably than its percent
  field did.
- Rename funding.yml to FUNDING.yml. GitHub requires the uppercase name, so
  the sponsor button was not rendering.
- Add an npm test script.
The v1 engine was a single fluent-ffmpeg call, ffmpeg(input).save(output),
with no codec, bitrate, resolution, trim, quality or stream-copy control at
any layer. These modules are the spine the 2.0 features need. They import no
Electron, so they are unit-testable without launching the app.

- formats.js   Capability graph replacing the within-group cross-product.
               Cross-kind conversions are now representable: video to audio,
               video to frames or a thumbnail, images to video. Subsumes the
               gif-to-still special case that was hardcoded in main.js.
- job.js       Job schema with defaults derived from target-format
               capabilities, nested-merge patching for bulk edit, and
               validation that rejects impossible combinations without
               requiring metadata to be present.
- ffmpeg-args.js  Pure buildArgs(job) -> string[]. Stream copy, crf/bitrate,
               presets, resize with contain/cover/stretch, trim via input
               seek plus duration, per-format image quality, a palettegen
               chain for GIF, frame patterns, faststart.
- runner.js    Concurrency pool, per-job ids, per-job cancel, pause/resume,
               partial-output cleanup, structured progress parsed from
               -progress pipe:1. Replaces the single activeConversion global.
- probe.js     ffprobe wrapper with normalised output. Treats metadata as
               enrichment, so a failed probe never blocks a job. Discriminates
               cover art from real video streams.
- paths.js     Output routing (alongside, fixed, mirrored source tree), name
               templating and conflict policies. Output was previously
               hardcoded to the input directory.
- scan.js      Async recursive directory walk with depth and count limits and
               extension filters, replacing a synchronous one-level readdir
               that ran on the UI thread and discarded nested files.
- pipeline.js  Node graph schema and filter_complex compiler with cycle,
               port-type and reachability validation. Schema and compiler
               only; the flowchart editor is deliberately not in this slice.

53 tests cover the argument builder, the pipeline compiler, path resolution
against a real filesystem, progress parsing and the directory scanner.
…ayer

The renderer ran with nodeIntegration:true and contextIsolation:false and no
preload, requiring fs, path and electron directly. It now has no Node access
at all; everything goes through window.electronAPI, matching the name the
Dropgate client already uses.

- preload.js exposes the bridge. It runs sandboxed, where require() is limited
  to a small allowlist, so its channel names are inlined rather than imported
  from constants.js. Requiring anything else there silently leaves
  window.electronAPI undefined and the app inert with no error in main, so
  test/preload.test.js guards both the allowlist and channel-name drift.
- IPC channels are namespaced by domain. Progress and status now carry a
  jobId, which is what makes concurrency possible; v1 pushed a bare integer
  with no way to tell which file it belonged to. The convert result is an
  object rather than an overloaded string|'cancelled'|null whose string could
  be either a file or a directory.
- The renderer no longer stats the filesystem to discover whether an output
  was a folder of frames; the result carries isDirectory and fileCount.
- Directory expansion on drop moves to the main process.
- Add a document-level drop guard. Dropping outside the zone previously
  navigated the window to the file, replacing the app with it.
- Fix dragleave firing on child boundaries, which made the highlight flicker.
- credits.html loses its require() and inline onclick handlers.
- Handle file associations properly: parse argv, add an open-file handler, and
  forward paths from second-instance, which previously discarded them so
  'Open with' on a running app did nothing. Explorer spawns one process per
  selected file, so arrivals are debounced.
- Extend electron-store beyond windowBounds with a real settings schema.
- Remove dead code: the unused get-version/get-setting/set-setting handlers,
  the globalShortcut import, and the .btn-accent, .bg-accent and
  .btn-format-disabled rules.

styles/tokens.css is the first real token layer in any of these apps, and it
fixes a live bug: --bs-primary was remapped to the accent only inside
[data-bs-theme=dark], so the primary Convert button rendered Bootstrap blue in
light mode and green in dark. It is now bound in both themes and on the bare
:root, which also covers the system-default case.
docs/design-system/ holds 20 generated artboards documenting the visual
language shared by Diamond Media Player, the Dropgate client and Diamond File
Converter, surveyed across the three codebases. Each card is a self-contained
page carrying a @DSCARD marker; build.js regenerates the set. The same bundle
is published as the 'Diamond Digital - Electron House Style' design system,
so the reference is not only version-controlled here.

Covered: the accent and token layer, the two wash formulas, typography,
spacing and radii, motion, buttons, drop zone, file row, toggle, status card,
progress, toast, modal, form rows, window chrome, popup windows, the Explorer
context menu, settings placement, feedback and theming.

The agreed house rule is per-app accent, everything else shared. Today the
apps use green, purple and stock Bootstrap blue, and none had a token layer,
so the accent was applied by hand-written hexes.
The v1 UI was a single column with one shared target format per media kind and
a strictly sequential convert loop. Jobs are now independently configurable
cards that run through the runner's concurrency pool, which has been sitting
unused since the architecture slice.

- Each card carries its own source, target, metadata and status. Cards render
  the moment a file is dropped and fill in duration, resolution, codec and
  size when the probe resolves, so a slow probe never blocks the grid.
- Selection with click, ctrl+click, shift+click ranges, ctrl+A, Escape and
  Delete. A plain click inside an existing multi-selection keeps it, so acting
  on a group does not collapse it first.
- The bulk bar offers only formats every selected source can actually produce
  - the intersection - so a bulk change cannot create an invalid job, and it
  says so when a mixed selection has no common target.
- Per-card progress and per-card cancel, both already supported by the runner.
  Unknown-duration jobs animate rather than sitting at 0%.
- Folder ingest is wired to the recursive scanner, and reports what it
  skipped. v1 discarded unsupported files in silence, which read as the app
  being broken.

Failures no longer open a blocking native dialog per file, which with a
concurrency pool would stack several at once and halt the batch. Each card
shows its own ffmpeg reason with a Retry action, and a single toast summarises
the run. The toast component existed only in the Dropgate server; this is the
first desktop implementation.

Error text is now selectable. body { user-select: none } is scoped to the
chrome so filenames, paths and failure messages can be copied - previously a
user could not copy an error in order to report it.

Two accent bugs fixed alongside: .progress hardcodes --bs-progress-bar-bg to
Bootstrap blue rather than deriving it from --bs-primary, so the overall bar
did not match the accent-green card bars. And Convert/Cancel were rendering
side by side, where the house pattern is for a primary action and its abort to
share one slot.

Window default widens to 1100x780; 700px fits only two cards per row.

core/display.js is dual-loaded - required by tests in Node, loaded as a plain
script by the renderer - so it takes no dependencies and receives the format
graph as an argument. Tests assert that property, that its STATUS mirrors
core/job.js, and that index.html loads it before renderer.js.
Selecting the "Choose format…" placeholder left the previous format in place.
The per-card select passes null for the placeholder, and setTarget ran that
through the same validity guard it uses for real formats — null is in no
source's target list, so the guard skipped the job and the target was never
cleared. The bulk bar had the same bug wearing different clothes: it tested
`if (e.target.value)` before calling setTarget, so choosing the placeholder
there did nothing at all.

Clearing is now handled explicitly and skips the guard, which only ever
applied to real formats. Two related cases fixed while in there:

- Re-targeting a job that had already finished, failed or been cancelled left
  its old status on the card, so a converted file still read "Done" and a
  failed one kept a stale error even though the new target had not run yet.
  Changing the target now returns the job to pending and drops the old output
  path, so the card reads "Ready".
- The bulk bar could set a target on a running job, whose select is disabled
  precisely to prevent that. Running jobs are now skipped.

Toast icons and dismiss buttons sat 1.5px above the optical centre of the
first line of text. material-icons ships line-height: 1, so an 18px glyph box
shared its top edge with the body's 21px line box rather than its centre.
Both now take the height of that line box and centre within it, measured at
0px offset across all four variants including a wrapped multi-line message —
where the icon must stay on the first line rather than centring over all of
them.
"1 still need a format" was the reported symptom. The audit found four such
bugs, not one, and six more places doing it correctly but by hand:

- "N still need a format" never agreed at one.
- A single extracted frame read "1 frames".
- A truncated folder scan read "1 files".
- "skipped N unsupported" had no noun to agree with at all.

The cause was that each call site wrote its own ternary, so getting it right
depended on remembering to. display.plural() and display.countOf() now own
this, and all thirteen counted strings go through them. Two tests guard the
pattern: one rejects the `n === 1 ? "file" : "files"` shape anywhere in the UI
code, the other rejects a raw interpolated count sitting directly before a
plural noun.

Writing the singular coverage surfaced a separate bug in the same area. The
action bar showed its outcome summary only while `converting` was true, so the
instant a run finished the bar snapped back to "1 file queued" — describing
files that had just been converted as still waiting. It now switches on
whether anything has actually settled, which also fixes the inverse case: a
run with nothing finished yet reported "Nothing converted", which is true but
reads like a failure, and now reads "Converting 3 files…". The detail line no
longer claims "Ready to convert" after a completed run.
Four interface fixes plus the first manual testing checklist.

**No UI text is highlightable.** This reverses a call made when the grid
landed: filenames, metadata, status text and toasts were deliberately made
selectable so an error could be copied. In practice a click-drag on a card
caught the filename and highlighted it, which makes the card feel broken —
selecting chrome is never what someone meant to do. Coverage is now complete,
with real form fields the only exception. Copying an error is a job for a copy
action on the context menu, not for making the whole interface selectable.

**Cards respond to hover** with a background wash as well as a border tint, and
a selected card that is also hovered keeps the stronger fill so it never reads
as less selected than a merely hovered one.

**Checkboxes in each card's top-left corner.** The whole card stays clickable,
but multi-select was previously only discoverable if you already knew ctrl+click
worked. A card's checkbox toggles just that card whatever else is selected. The
bulk bar gains a select-all checkbox that shows an indeterminate dash on a
partial selection.

**Buttons that sit together now share a height.** Clear All and Cancel were
btn-sm next to a full-size Convert, so Clear All looked stunted. All three
measure the same, and the check is asserted rather than eyeballed.

The bulk bar is now always visible once there are cards, which is the more
substantial change. Feedback on the grid was that the previous version could
convert a pile of files to one format in two clicks, and the first cut of the
grid required selecting the cards first — making the most common case worse in
order to serve the rarer per-file one. With no selection the bar reads "All N
files" and applies to everything, so the two-click flow is back; selecting
narrows the same control to the selection. Nothing per-card was given up.

Adds docs/MANUAL-TESTING.md, a checklist for a human covering what the
automated tests cannot judge: appearance in both themes, drag and drop, real
dialogs, Explorer integration, installer behaviour, and the specific
regressions each fix addressed. A section is appended per version from here on.
Two defects from the alpha.1 QA pass, plus the updater rule for pre-releases.

**Escape now closes the Credits window.** It was handled by a keydown listener
inside credits.html, which worked unpackaged but not once packaged — a page
listener depends on that script having run and on focus sitting in the
document, neither of which is guaranteed. It moves to before-input-event on the
window's webContents in the main process, which sees the key regardless. The
page handler is removed rather than kept as a fallback, so a regression in the
main-process one cannot be masked. Verified against a packaged build by driving
real Windows key input: Credits opens on its accelerator and closes on Escape.

**A colliding output prompts instead of silently renaming.** The Cancel /
Overwrite / Save as New dialog existed but was unreachable: main.js built each
job without the user's conflict setting, so createJob's own default of "unique"
applied and every collision quietly became "name (1).ext". The setting is now
threaded through, and both it and createJob default to asking.

The prompt gains an "Apply to all remaining files" checkbox. Jobs run through a
concurrency pool, so without a way to answer once for the batch, converting
fifty files into a folder that already holds them would have meant fifty modal
dialogs. The choice is cleared when the pool drains so it cannot leak into the
next run.

**Pre-releases are never offered as an update.** This needed to be explicit:
electron-updater turns allowPrerelease on by itself when the running version
carries a pre-release tag, so shipping 2.0.0-alpha.1 would have opted every
alpha tester into being updated to the next alpha. allowPrerelease,
allowDowngrade and the channel are now pinned, and update-available re-checks
the offer so a mis-tagged GitHub release cannot push an alpha at everyone.

The manual update check was also wrong in a way that only shows up on a
pre-release: it compared the found version to the running one for equality, so
someone on 2.0.0-alpha.1 with a latest stable of 1.0.0 got neither "up to date"
nor an offer — the check silently did nothing. core/version.js now owns the
comparison, with tests covering pre-release ordering and the rule that only a
strictly newer stable release is ever offered.

Adds a QA-fixes section to the manual testing checklist covering all three,
noting that the Escape case has to be tested against a packaged build.
The engine has supported codec choice, constant quality, bitrate, encoder
presets, resize with fit modes, frame rate, trim, stream copy, output routing
and name templating since the architecture slice. None of it was reachable.
This is the interface for it.

Every control is built from the target format's own capability descriptors, so
a container is never offered a codec it cannot carry: WebM offers VP9 and not
H.264, MP4 the reverse, MP3 only its own. Sections appear and disappear with
the target — an audio target has no video controls, an image target has no
audio, a still image has no trim while a GIF does. Invalid combinations are
unreachable rather than merely rejected on submit.

A live command preview runs the spec through the real argument builder in the
main process, so what the dialog shows is what will actually be executed
rather than a reconstruction that could drift from it.

The dialog edits a *partial* spec. Anything left untouched is omitted entirely
and createJob fills it from the format's defaults, so the dialog does not
freeze today's defaults into every job. Applying with nothing changed leaves
the job exactly as it was, and the card shows no settings summary.

Deliberately not the only way to set a format. The per-card dropdown and the
bulk bar still work, so "drop a folder, pick a format, go" stays two clicks —
the point raised when the grid landed.

Presets save and restore the whole configuration including the target format,
through the store bindings that were already wired.

Cards gain a tune button that opens the dialog for that one file, and a green
summary line describing what has been configured.

Three bugs found by driving the dialog rather than by reading it:

- CRF was silently discarded whenever the codec select sat on "Format
  default". The effective codec is the chosen one *or* the container's
  default, and treating the empty select as "no codec" disabled constant
  quality and dropped the value on apply.
- Switching to a codec with no constant-quality mode forced the control to
  bitrate and never released it, so returning to H.264 left it stuck and threw
  the CRF away again.
- Both stream selects rendered blank. Assigning a value no option carries
  leaves selectedIndex at -1, which reads back as "" and collapsed the whole
  encode section, since those fields only show when the mode is "encode".

Verified end to end against ffmpeg: a spec shaped exactly as the dialog
produces one resizes, re-times, trims and downmixes the real output.
…ction bar

Review feedback on the advanced settings dialog.

**"Job" is gone from the interface.** One card opens **Advanced options**; a
selection opens **Bulk edit**. The dialog titles itself according to how it was
opened. Internal identifiers keep the word — core/job.js, JobRunner, the job:*
channels, .job-card — because renaming them is a large diff with no user
benefit and real regression risk, and none of it is visible. That is recorded
as a rename-only commit if it is ever wanted.

**Bulk edit moved out of the corner and into the green selection bar.** The bar
already answers "what am I acting on", so the action that acts on it belongs
beside the format dropdown rather than in a separate corner. It follows the
same scope rule: the selection, or everything when nothing is selected, where
it reads "Edit all". Options are already restricted to what every file in scope
supports, since the target list is the intersection and every codec, quality
and section is derived from that shared target.

**"All 1 file" became "Select all"**, which is what the checkbox beside it
actually does.

**Trim is a two-point slider.** Two text boxes never showed the useful thing,
which is where the kept span sits relative to the whole clip. Handles cannot
cross, the kept duration is shown between them, and holding Shift while
dragging scales pointer movement down for fine adjustment. Arrow keys move a
handle, Shift+arrow moves it less, Home and End jump to the extremes. A file
with no readable duration says it cannot be trimmed rather than presenting a
slider with nothing to slide over, and a bulk edit spans the longest clip in
the selection.

**The dialog's Cancel was filled.** A neutral action beside a filled primary is
outlined — two filled buttons side by side read as two equal choices, which is
exactly what a cancel is not. The design system said the opposite, because
Bootstrap's own modal example uses a filled btn-secondary and that is what the
Dropgate client copied; the buttons and modal artboards now carry the rule and
a correct/wrong pair.

Bulk editing files with differing settings now starts blank and says so, rather
than presenting one file's values as if they applied to all.
…ptions

A fixed max-width on a <select> cuts the selected option off with no ellipsis
and no scrollbar — the text simply disappears, which is how "Contain" was
showing as "Contai" and "Constant quality" as "Constant qual". Both were my
own inline max-widths. The rows they sit in now span the dialog grid rather
than being constrained, and the selects keep a min-width floor so flex cannot
shrink them below their own text.

Rather than fix only the two that were spotted, this adds a sweep that
measures every visible control against the text it is trying to show and fails
if any is clipped: selects against their selected option, inputs against their
value or placeholder, everything else on scrollWidth. It runs across six target
formats, two window widths and both themes. It found exactly those two and
nothing else, and now passes everywhere.

Worth noting the narrow window was never affected — the dialog grid collapses
to one column and gives each control more room, so this only showed at the
wider layout where three columns compete.

Presets are gone from advanced options. Advanced options are configured
deliberately per file, because every file and workload differs, so there is
nothing there worth saving. Reusable, saveable configuration belongs to
pipelines: build one, then assign cards to it before starting. The
preset:list|save|delete channels and their store bindings stay put — that is
the persistence pipelines will use, so only the misplaced dialog UI was
removed.
A card is configured by hand or handed to a saved pipeline, never both. Two
tiles at the top of the advanced options dialog make that choice, and the
sections below swap with it.

Built on the Craftbox create-server pattern, with three deliberate departures.
It is worth saying plainly that the pattern is not a radio group — that app has
no radio inputs, no btn-check and no btn-group anywhere; it is tiles with
role="button" and a click handler recomputing a selected class from one state
variable.

- **Theme-aware.** The original's idle border is a fixed white alpha, which
  only works because that app is dark-only. This uses the border token so it
  survives light mode.
- **Strictly binary.** The original is tri-state: clicking the selected tile
  deselects back to "from scratch". That works there because a third state
  exists. With exactly two modes, deselecting would leave the form in no mode
  at all, so re-clicking the active tile does nothing.
- **Keyboard operable.** The original has role="button" and nothing else — no
  tabindex, no aria-pressed, no key handling — so its tiles cannot be reached
  or activated from a keyboard at all. These carry all three plus a focus ring,
  and a locked tile leaves the tab order.

Borrowed as-is: fail closed, then unlock. The Pipeline tile ships locked and
explains why, and is enabled once a pipeline exists, the same way Craftbox
locks Template until one is saved. The lock is enforced in the handler as well
as the CSS — pointer-events stops a mouse but not a keyboard or a programmatic
activation, which an integration check caught.

Switching modes is lossless. Manual settings are retained while a card is in
pipeline mode and restored on switching back, so comparing the two approaches
costs nothing. Only the active side is ever used, and the card shows only the
active side — a pipeline card names its pipeline rather than displaying dormant
settings that will not run.

A card whose pipeline has since been deleted reads "Pipeline missing" in amber,
the same treatment as a card with no format, and blocks Convert. It does not
fall back to the manual settings it still holds; converting with settings the
user did not ask for is worse than refusing.

The card field is called `processing`, not `mode`. core/job.js already uses
job.mode for the conversion mode, and the card's fields are spread into the run
spec, so a clash would have silently overwritten it.

**Fixes a real bug in the pipeline compiler, found by running one for the first
time.** Every stream was mapped with brackets, including untouched ones, so a
passthrough audio stream came out as `-map "[0:a]"`. Brackets denote a
filter_complex output label, so ffmpeg hunted for a label that did not exist
and refused the output with "Error opening output files: Invalid argument".
Only filter outputs are bracketed now. Two existing tests had encoded the
buggy form and were corrected.

Adds a mode-tiles artboard to the design system, since this is the first mode
chooser in any of these apps and the next one should match it.

The manual testing checklist gains a section for this, and every section now
carries an explicit "not started" status. Nothing in that document has been
executed — testing is on hold pending UX sign-off, and a written checklist must
not be mistaken for a passed one.
The advanced options dialog, the selection-bar bulk edit, the clipping sweep
and the manual/pipeline mode chooser have all landed since alpha.1. Nothing in
docs/MANUAL-TESTING.md has been executed yet, so this build ships as an
untested preview.
The first hands-on pass on real Windows reported nine issues. Seven reproduced
in the code and are fixed here; two did not, and are recorded as open rather
than papered over.

**Escape, Delete and Ctrl+A were swallowed after choosing a format.** The
global key handler treated any focused SELECT as "the user is typing" and bailed
out. A select keeps focus after you pick an option, so choosing a format on a
card ate the next key until something non-focusable was clicked. Narrowed to
TEXTAREA and text-bearing INPUTs — a checkbox is an INPUT too, so the type is
tested rather than just the tag. The dialog now also owns the keyboard while it
is open, or Delete removed the very cards being edited behind it.

**Convert re-ran finished batches.** startConversion took every card with a
format, so fixing one failed file and pressing Convert resubmitted the whole
completed batch and collided with every output the previous run had written.
It now skips cards that are already done; Retry is what re-runs one. The button
needed its own count, since `ready` includes finished cards and drives the
"still need a format" copy.

**"Apply to all remaining files" had no effect.** Not the flag — a race. The
concurrency pool brought several jobs to the resolver at once, and each read
the batch choice (still null) and opened its own dialog before the first answer
came back, so the tick could not reach the prompts already queued behind it.
Prompts are now serialised, and each re-reads the choice after the previous one
settles.

**One defect caused both reported Advanced-options bugs.** syncModalControls
rebuilds the codec selects from dataset.wanted, which was only written when the
dialog opened — so a pick was overwritten by the rebuild it triggered itself.
The codec was therefore dropped on Apply, which is worse than the cosmetic
label mismatch it was filed as. It also explains Quality appearing stuck on
"Target bitrate": with the select reset, the effective codec fell back to the
container default, and AVI's is mpeg4, which has no constant-quality mode. The
existing restore logic was correct and never got the chance to run.

**An unreadable file sat on "Reading…" forever.** probe() resolves { ok: false }
rather than rejecting, and describeMeta returned null for that exactly as it
does while a probe is still in flight. Pending and failed are now distinguished.

**DevTools were unreachable.** Electron's F12 accelerator comes from the default
application menu, so replacing it with a custom template removed it. devTools
was never disabled in webPreferences, so this was an accident rather than a
lock-down; re-declared explicitly.

**Two reported defects did not reproduce, and are not claimed as fixed.**
Both mechanisms were driven directly under Electron. Escape on the Credits
window: before-input-event fires and the window closes, with the webContents
holding focus. What was missing is a guard against opening the window more than
once — every menu click built another modal, so Escape closed the top one and
uncovered an identical one behind it, which reads exactly like Escape doing
nothing. That guard is added. Live theme change: the page's
prefers-color-scheme listener fires, matches flips and the page re-themes
itself. The OS-broadcast path cannot be exercised without changing the
machine's own theme setting, so a push from nativeTheme is added as a second
route; it is hardening for an unconfirmed report, not a reproduced fix.

The manual testing checklist gains a section covering all of it, including
steps aimed at establishing whether the two unconfirmed defects exist at all.
Nothing in that document has been executed since the first pass.
Ships the seven fixes from the first manual test pass. The checklist section
for them is retitled to the release that actually carries them; nothing in that
document has been executed since the pass that found them.
alpha.2 shipped everything downstream of a pipeline — the Manual settings /
Pipeline tiles, the card's processing mode, the amber "Pipeline missing" state,
and the IPC folding a saved graph into a job. What it never shipped was any way
to make one, so the Pipeline tile has been locked in every released build with
nothing able to unlock it. This closes that.

A pipeline binds to one card and one input file, which is what keeps the editor
a pure view layer: the compiler, the job model and the argument builder are all
untouched. Joining several files together is a separate surface, so the palette
omits Concatenate and Overlay — both still exist in the model and still compile,
they simply cannot be added.

The window gains a collapsible navigation rail. Files and Pipelines are siblings
you switch between rather than a view you back out of, which is also what the
Join surface will slot into. Collapse is remembered between launches. One
function owns every route between views, so the unsaved-work guard cannot be
sidestepped — a rail click, the link from the advanced options dialog and a file
drop all pass through it, and a declined guard cancels the drop rather than
ingesting behind it.

core/pipeline.js gains, all additively:

- The same UMD wrapper display.js uses, so the renderer loads it as a plain
  script. It requires nothing, which is what makes that safe, and a test now
  enforces that it stays that way. Validation and the command preview are
  synchronous as a result, with no IPC round trip.
- A params schema on every node type, so the inspector is generated rather than
  hand-written per type. A second hand-maintained table of the same facts would
  drift from the first.
- canConnect(), which refuses what the model tolerates but ffmpeg does not: a
  type mismatch, an input that is already connected (compile takes the first
  edge and drops the rest in silence), one output fanned into two nodes (a
  filter label can only be consumed once without split/asplit), a
  self-connection, and a loop. Each says why, in the app's own words, at the
  moment of the drop.
- autoLayout(), placing nodes by dependency depth and leaving alone any the user
  has already moved.

Node positions ride on the graph as node.ui, which validate and compile ignore
and the store round-trips untouched.

Also fixes a defect the editor exposes: validateJob ran before a pipeline was
folded into the job, so an encode node naming a codec the target container
cannot carry reached ffmpeg unchecked. Both the run and the preview paths now
look again afterwards, and the card fails with a readable reason instead.

Verified by driving the real app end to end — building a pipeline, saving it,
assigning it to a card and converting, with ffprobe confirming the output is the
size the pipeline described and that the untouched audio stream survived. The
bracket invariant is checked there too: a filtered stream maps as [v0], an
untouched one as 0:a.

Manual testing steps for the rail and the editor are in docs/MANUAL-TESTING.md.
The nav rail and node canvas patterns are recorded in the design system.
Pipelines went out in alpha.4 and are being taken straight back out. The feature
was out of proportion to what this app is for — a node-graph editor is a large,
fiddly thing to learn in order to do something the advanced options dialog
already does per file — and in use it was unintuitive. The alpha.4 release and
its tag have been deleted, so that version number is free again.

Removed: the editor view, the Manual settings / Pipeline mode tiles, the graph
model, compiler and validator in core/pipeline.js, the pipeline:* IPC channels
and their store bindings, the card's processing and pipelineId fields, and the
"Choose a pipeline" / "Pipeline missing" states in the display layer. A card is
configured by hand, full stop.

Kept deliberately:

- The navigation rail, with Files as its only destination. Joining several files
  into one is still wanted and will sit beside Files rather than being somewhere
  you have to back out of. A rail with one destination costs a strip of width
  and promises nothing it cannot do, which a greyed "coming soon" item would.
- The alpha.3 fixes that happened to live next to the removed code — the codec
  selects rebuilding themselves, the serialised conflict prompts, and the rest.

The window minimum was 720x560, which let the selection bar run past the right
edge and clip its own buttons. Measured rather than guessed: the file grid
starts to break below about 800px wide. Now 880x600, which leaves headroom and
still shows two full rows of cards.

Two things the removal broke and this restores: the "Convert to" format row sat
between the mode tiles and the Output section and went with them, and a leftover
`manual` flag in syncModalControls stopped the trim slider being set up. Both
were caught by driving the real app rather than by reading the diff.

126 unit tests pass. Verified end to end: the app starts with no console errors,
nothing pipeline-shaped survives in the DOM or on the IPC bridge, the advanced
options dialog opens with no chooser, and a real file still converts.
preset:list|save|delete were wired to electron-store in the 2.0 architecture
slice and never given a UI. They were kept through the pipeline work on the
grounds that pipelines would be their consumer; pipelines are gone, so nothing
is ever going to call them.

Removed with them: the "presets" key in the store defaults, and the {preset}
name-template token in core/paths.js. Nothing ever populated context.preset, so
that token silently expanded to an empty string — which is precisely the
behaviour the comment above it says the module avoids, since an unrecognised
token is meant to survive into the filename where a typo can be seen. It now
does, because {preset} is no longer recognised.

The encoder preset (-preset slow and friends) is a different thing entirely and
is untouched.
…cted

**Drag and drop has never worked in 2.0.** Every drop threw "files is not
iterable" before a single file reached the queue. getPathsForFiles took the
whole FileList and iterated it inside the preload, but a FileList is not a type
the context bridge can carry — it arrived on the other side as a plain object
with no iterator. Individual File objects do cross intact, so the iteration now
happens in the renderer, where the FileList is still a FileList, and the bridge
resolves one file at a time.

This predates the pipeline work and was not caused by removing it. It survived
because the drop checks live in the alpha.1 section of the manual checklist,
which has never been run, and because using the Add Files button takes a
different path entirely and always worked.

Reproduced before fixing, by dispatching a real OS-level drag through the
debugger protocol rather than a synthetic event — a synthetic one carries no
file the resolver can resolve, so it cannot show this bug.

**The bulk bar now only offers actions that apply.** With nothing selected it
showed an "Edit all" button, which is a whole-queue edit nobody asked for from a
bar that is meant to be read as "here is what applies to everything". Bulk edit
now appears only alongside Remove and Deselect, once a selection exists. The
Select all checkbox, the "Convert all to" label and the format dropdown all stay
put with no selection — that is the two-click flow and it must not gain a step.
The button's label no longer changes, so its span id went with it.
…work

The queue described item 6 as "cross-kind conversions are not surfaced — remove
the sameKind filter". That premise was years stale: target lists have been
driven by the full capability graph since the card grid landed, and MP4 has been
offering all 21 targets, 14 of them cross-kind, with tests asserting it. The
filter only ever affected buildLegacyConversionMap, which the renderer uses to
decide whether a dropped file is supported and which icon to draw.

So the conversions were offered and then quietly did the wrong thing. This is
the correctness half; the frame-span UI follows.

An animated source keeps its motion into a video container. allowedModes mapped
image->video to ASSEMBLE on kind alone, so an animated GIF asked for MP4 was
treated as one still and looped for five seconds — the animation discarded.
Worse, ASSEMBLE emits -loop, which belongs to the image2 demuxer; the gif
demuxer has -ignore_loop instead. An animated source now transcodes.

Whether a file moves is decided by the probe, not by its extension. gif and webp
*may* hold an animation and usually do not, and allowedModes can only see two
extension strings. An ordinary still WebP asked for PNG therefore took the
animated path and landed as a *directory* containing one frame. createJob now
resolves the default mode from inputMeta when it has one, and main re-resolves
after its own probe.

An image source is never asked to carry audio. The old check tested for
ASSEMBLE, which stopped covering an animated GIF the moment one transcoded.

A single frame is taken from the middle of the video rather than from frame
zero, which is a fade-in or a black slate often enough to be the wrong default
for something whose whole purpose is to represent the clip.

Frame files are numbered %06d. ffmpeg widens the field rather than truncating,
so %04d ran frame_9999 straight into frame_10000 and the directory stopped
sorting in capture order exactly when there were enough frames to care.

Choosing frames out of a video is offered as a span. applicableSections hid the
trim control for every still target, which is backwards: a PNG from another
picture has nothing to choose, but a PNG from a video is entirely a question of
which frames. It now takes the source and the mode, not just the target.

Stream copy is offered only when it would work. Every audio container claims
stream-copy support, so extracting audio always offered "copy without
re-encoding" — which fails inside ffmpeg unless the stream already fits the
target. The check needed an encoder-to-codec mapping: the format table names
encoders, a probe names codecs, and ffmpeg's own -encoders output spells out the
three that differ ("libmp3lame ... (codec mp3)").

Retargeting across kinds drops settings chosen for the old target. Verified
before the fix: a card configured with libx264 and retargeted MP4 -> PNG failed
with "PNG does not support the video codec libx264", a setting the user never
asked to apply to a picture.

Also removed, all unreachable: job.inputIsSequence (read by buildArgs, never set
by createJob, so assembling a numbered sequence could not happen — it needs
several cards to become one job, which is item 5's problem), job.extraArgs (the
pipeline compiler's channel onto a job), STATIC_IMAGE_EXTS, and the legacy map's
same-kind targets array, which no longer had a reader and implied the UI was
still same-kind only.

144 tests pass, 18 of them new, including a matrix guard over all 867
source/target/metadata combinations asserting that display.js's effectiveMode
and core/job.js's defaultModeFor never disagree — display.js cannot require the
job model, so the two are policed the same way STATUS is.
The second half of cross-kind conversions. The first made them correct; this
makes the one with a real decision in it usable.

Video into stills is the only conversion where the mode genuinely matters —
every frame, or one — and it was unreachable. Every target descriptor carried a
`modes` array the renderer never read, so video to PNG was always *all* frames
and THUMBNAIL could not be reached at all.

There is no mode dropdown. The trim slider becomes a span with a decoded frame
shown at each end, and the choice between every frame in that span and a single
one *is* the mode decision. applicableSections already knew the target; it now
knows the source and the mode too, because the same target means different
things depending on where it came from: a PNG from another picture has nothing
to choose, a PNG from a video is entirely a question of which frames.

Frame previews come through a new preview:frame channel — one decoded frame at
a timestamp, as a data URL. Debounced and token-guarded, because dragging a
handle asks for a great many frames in quick succession and a slow decode
landing late must not paint a frame the user has moved past. A source that will
not decode simply shows nothing; a missing preview must never block the choice.

The preview of the far end seeks just inside the duration. The end handle sits
exactly at it, which is past the last frame, and decodes nothing at all.

Frame extraction could write thousands of files from one click with no warning.
The card now states the count, and Convert asks once above a thousand. The count
is the honest lever too: narrowing the span is how you make it smaller, which is
why the card's line opens the dialog.

A picture target no longer asks for a video codec, a target bitrate or an
encoder preset. Exporting frames to PNG rendered the whole encoder block above
the controls that decide anything — the codec dropdown holding exactly one
entry. Resize and frame rate still apply, so this is narrower than hiding the
section, and it is titled Picture rather than Video.

Two bugs found by driving the real app rather than reading the diff. setupTrim
only ran when sections.trim was true, which a frames conversion sets false — the
slider was never wired up at all. And compactSettings dropped any top-level
value that was not an object, so the scalar `mode` was silently thrown away on
Apply and the choice never reached the job.

145 tests pass. Verified end to end against real ffmpeg output: a narrow range
writes exactly that range in sortable order, a single frame is one file with
real content in it, and the count on the card matches what lands on disk.
Three fixes from the cross-kind test pass.

**The frames readout said "keeping" until something moved.** framesMode() read
back a CSS class that renderFramesChoice sets, but the slider paints first — so
on the first open the class still described the previous conversion and the
readout used trim wording for a frame range. It is held as state now, set before
anything paints, rather than recovered from the DOM.

**A failure reported ffmpeg's verdict instead of its reason.** lastMeaningfulLine
took the last non-empty stderr line, which is reliably "Conversion failed!" —
ffmpeg always closes with it. The genuine explanation sits several lines earlier,
behind the progress counters and a line describing the consequence rather than
the cause. It now skips those and prefers the first line that names a cause,
stripping the "[component @ 0x7f...]" prefix that means nothing to a user. An
animated WebP now reports "skipping unsupported chunk: ANIM" rather than nothing
at all.

**An animated WebP is refused before the conversion is configured.** The bundled
FFmpeg 6.1.1 cannot read one — its WebP decoder skips the ANIM/ANMF chunks — and
ffprobe describes such a file as 0x0. A picture stream with no measurable
dimensions is one ffmpeg cannot decode, so the probe reports it as unreadable
with a reason and the card says so as soon as it returns. This is generic rather
than WebP-specific: an audio file has no picture stream to measure, and cover
art has real dimensions, so neither is affected.

A third fix, found while testing the first: isSingleFrame() consulted the frames
choice even when the conversion had none, so changing a target from PNG back to
MP4 left the readout blank — and would have thrown away the trim's end, since a
single frame deliberately carries no span.

The manual checklist claimed animated WebP to PNG should produce a folder of
frames. It cannot, and would have sent the next tester after a defect that is
not ours; corrected, with the limitation recorded in the queue against any
future ffmpeg bump.

152 tests pass. Verified in the real app: the readout is right on first open,
every cross-kind conversion still produces what it did, and an animated WebP is
caught at probe time rather than at conversion.
Everything that moves within its current major, taken without ceremony:
electron-builder 26.8.1 -> 26.15.3, electron-updater 6.8.3 -> 6.8.9.

Electron went 41.1.0 -> 44.0.0, three majors, bringing Chromium 152 and Node 24.
**No application code changed for it.** That was established by evidence rather
than nerve: unit tests pass on any Electron and prove nothing about a runtime
bump, so the real app was driven against the things a major version actually
threatens.

- webUtils.getPathForFile across the context bridge, exercised with a real OS
  drag dispatched through the debugger protocol rather than a synthetic event —
  the distinction matters, since a synthetic drop carries no file the resolver
  can resolve and would pass while broken.
- Spawning ffmpeg and piping a decoded frame back over IPC for the previews.
- Bootstrap's modal machinery, via the frame-count confirmation.
- Live theming, and every cross-kind conversion end to end with the output
  inspected by ffprobe.

The one deprecation that surfaced is in test tooling, not the app:
webContents.on("console-message") now passes an event object, and the old
(event, level, message) signature warns.

The installer is about 308MB, up from 242MB. That is Electron 44's own growth
and wants saying in the release notes rather than being discovered.

allowScripts is kept, and re-keyed against what is installed — it pins exact
versions, so it goes stale the moment a dependency moves and the lifecycle
prompt comes back.

Deliberately not touched: the transitive @electron/* packages (asar 3->4, get
2->5, notarize 2->3, universal 2->3, fuses 1->2). They belong to app-builder-lib
and electron-winstaller, and forcing them ahead of their parents breaks a build
for no benefit. They move when electron-builder moves.

152 tests pass and the installer builds and verifies. A manual section is added
for the runtime change; the same sweep is queued for the other projects.
The alpha.1 and alpha.2 sections had been held at "not started" since
2026-08-28, waiting for the UX direction to settle before anyone spent a
pass on a surface that might still move. It has settled, so they were
worked through against alpha.4 along with the three alpha.4 sections.
Every section now carries a PASSED status and every box is ticked, 307 of
them.

Two items are worth naming because their own prose said they had never
been run, and that prose is now wrong: drag and drop from Explorer, which
automation cannot drive out of a restricted Explorer window and which had
been flagged and deferred repeatedly, and converting end to end from the
installed build, which needed a reinstall from the clean 2026-09-02
installer because the copy installed before it was the polluted 08-31
build. Both are recorded against the date and the person who ran them
rather than being silently ticked.

The withdrawn "Processing mode (Manual settings / Pipeline)" section is
deleted rather than left in place, 76 lines of it. It was kept after
09eaca3 as a record of what was tried, but it describes tiles, a chooser
and a compiled-pipeline command preview that no longer exist anywhere in
the app, and a checklist is read by someone looking for work to do. The
record of why pipelines went belongs in that commit and in the release
notes, not in a list of steps nobody can follow.

The file's own rule is unchanged and still at the top: a written
checklist is not a passed one, and no build should be described as tested
on the strength of this file existing.
A job waiting on the "If it already exists" prompt was in neither `active`
nor `queue`, so the pool could not see it at all. Three defects follow from
that one fact, and they are fixed by giving the job a slot to hold while it
waits rather than by patching each symptom.

**"Apply to all remaining files" was discarded mid-batch.** _checkIdle tests
active.size and queue.length, both zero when every job is parked at a prompt,
so the runner announced it had finished while the user was still reading the
first dialog. main.js clears the batch-wide choice on idle. Measured before
the fix: five files at concurrency two, one tick of the checkbox, four
further dialogs.

This is the same symptom a tester reported against alpha.1. e4e16e5 fixed
the original — main built the job without the user's setting, so createJob's
own default of "unique" applied and every collision silently became
"name (1).ext" — and 4f1d06b fixed a race after it. Both were real. This is
a third route to the same place, and it survived because the checklist case
for it uses six similar files, which all finish probing before the first
answer. That is the one ordering that works.

**Concurrency did not apply to anything that took the ask path.** Nothing
re-checked the pool between the answer and the spawn, so once a batch answer
was set every parked job spawned in the same tick: eight ffmpeg processes at
a limit of two.

**Cancel All during a prompt still ran the conversion.** _cancelledAll was
read on entry and never again, so answering the dialog spawned ffmpeg
regardless and the job was marked cancelled in the close handler — after it
had overwritten the file.

The reservation makes _pump mandatory on the two pre-existing early returns
after the await. They got away with not pumping while a parked job held no
slot; now one does, missing it would stall the queue for good. There is a
test for that specifically, because it is a regression this change
introduces rather than one it fixes.

Left alone deliberately: cancel(jobId) for a job at a prompt still returns
false and does nothing. The dialog is OS-modal and cannot be dismissed from
here, so there is genuinely nothing to cancel until it is answered. Now
commented rather than silent.

Also drops the renderer stripping onConflict === "ask" before sending. It is
free today because the app-wide setting is "ask" too, so main reinstates the
same value. It stops being free the moment a global conflict preference
exists: a card set explicitly back to Ask me would inherit a global
Overwrite instead. Routing keeps its strip, and the asymmetry is commented.

Tests: five behavioural ones in a new test/runner.test.js, driving a real
JobRunner with an injected spawn. Confirmed to fail against the old code
before the fix, which is the only way to know a concurrency test is testing
anything. Two tests deleted rather than kept — both read main.js as a string,
and one asserted the presence of the very line that caused the first defect,
reading it as proof the behaviour was correct. Prompt ordering needs a real
dialog and moves to the manual checklist. The wiring guard that main still
threads the setting through stays, retitled to say what it is.

155 tests pass.
Dropping a folder reported "skipped 2 unsupported files" and nothing else,
so you knew something had gone missing but not what, and not whether it was
the app's limitation or a filter of your own. v1 discarded them in silence
and that was worse, but a bare count is not much of an answer.

scanPaths now returns skipped as { path, reason } entries rather than a
counter, matching the shape errors already used. Three reason codes -
unsupported, not-included, excluded - and no sentences: this module has no
business deciding how any of it is worded. A file reached twice is still not
counted as skipped, because nothing was dropped and there is nothing to say.

The toast gained an optional action, and the skipped list is that action
rather than a panel of its own. A panel would need a position, a dismissal
model, a focus model and an Escape handler in order to report that four
files were ignored - and the house rule is that anything merely reporting
what already happened is a toast. Filenames in the list override the
app-wide user-select: none, since a filename you cannot copy is not much use
in a bug report. Everything is still set through textContent; a toast
carries filenames and ffmpeg's own words, and neither is markup this app
gets to trust. A toast with an action no longer times out by default.

Two reporting bugs go with it, both from the branches being an else-if
chain over conditions that are not mutually exclusive:

- A scan that both truncated and skipped reported only the truncation. The
  clauses now compose, so it reports both in one sentence.
- Adding files that were every one of them already queued said nothing
  whatsoever, which is indistinguishable from a drop that never registered.
  It now says so.

And there were two different "skipped" toasts - one in the scanner path, one
in addFiles' own format check - which could both fire for a single folder
and word the same thing differently. addFiles no longer reports anything; it
returns { added, rejected, duplicates } and the caller merges its rejections
with the scanner's into one message, because they are one thing to the
person reading them.

Tests: reason codes per skip, a filtered-out file distinguished from an
unreadable one, and a dedupe asserted not to count as a skip. The existing
scan test is updated for the new shape - it was the only other consumer.

156 tests pass.
scanPaths has supported a recursion switch, extension filters, a symlink
switch, a depth limit and a file cap since the 2.0 architecture slice landed.
None of it was reachable: ingestPaths passed a literal empty object, and had
done since the day it was written.

Three of them are now on a popover beside Add Folder - include subfolders,
which kinds to add, and whether to follow shortcuts. They persist in the
settings store, which already shallow-merges whatever it is given, so this
needed no new IPC and no new channel; the options parameter was already
threaded renderer to preload to main to scanner.

Add Folder keeps its own click and opens the picker as it always did. The
options get a separate caret beside it. Adding a folder is the two-click
path and it does not get to grow a step because a setting exists.

maxDepth and maxFiles are deliberately not offered. They are safety rails,
not preferences - nobody opens a popover to decide how deep sixteen levels
is - and the toast that fires when one of them bites explains itself at the
moment it matters, which a number sitting in a menu cannot.

The options apply to every ingest route rather than only the button, because
a plain drop can contain folders too and having a setting cover one folder
route but not the other is indefensible. The cost is that a dropped .jpg is
skipped while Images is off, which is only acceptable because the previous
commit made the toast name that as the reason instead of dropping it in
silence.

Unticking the last remaining kind is refused. A filter admitting nothing
would skip every file in the folder and then carefully explain why, which is
honest and useless.

Also fixes the queue's dedupe, which compared raw path strings. Two
spellings of one Windows path are the same file - the separators can go
either way and the case is not significant - and the scanner has always
keyed its own dedupe that way. So a file already queued by a folder walk
could be queued a second time by being dragged in directly, giving two cards
for one file. Found by driving the real app rather than by reading the diff:
the folder walk produces backslash paths and the direct add did not, and the
new duplicate counter reported them as newly added.

Verified in the running app: the popover opens clear of the window edge and
closes on an outside click and on Escape, unticking Images skips a .png with
the reason "turned off in folder options" rather than "not a supported
format", unticking the last kind refuses, and one file reached three
different ways produces exactly one card.

156 tests pass.
"Mirror the source folders" has been in the Destination dropdown since
alpha.2 and has never worked in any released build. paths.js resolves it
from output.mirrorRoot, validateJob rejects a mirror job that has no root,
and nothing anywhere ever set one - so choosing it replaced the command
preview with "Mirrored output was selected but no source root was given" and
disabled Apply, with nothing the user could do to proceed.

scan.commonRoot has existed and been tested since the architecture slice,
with zero callers. This is the join that was missing.

The root is the folder that was handed in, not the common ancestor of the
files that came back. Those are the same only when the folder has more than
one populated subtree. Point at a folder holding just 2024/holiday/clip.mp4
and the ancestor of the found files is 2024/holiday, so the mirror would
flatten away the two levels the user asked to keep - the failure would look
like the feature working, on the folders where it matters least.

So scanPaths now reports the root itself: the directory it was given, or the
files' shared ancestor when it was handed loose files and there is no better
answer. commonRoot is now a thin wrapper over a commonDir that works on
directories, which keeps its signature and its existing test intact.

The root rides on the card rather than in job.settings, because
applyJobModal writes one settings object across a whole selection while the
root belongs to the ingest that produced each card. It is threaded at both
send sites - the run and the preview. Missing the preview would leave Apply
disabled, which is the original bug wearing a different hat.

Where there is no root at all - cards from folders on different drives -
mirror is disabled in the dropdown and says why, rather than being offered
and then failing validation. A file added on its own keeps it: its own
folder is a perfectly good root, and the output simply lands flat.

Verified end to end in the running app against real ffmpeg: a folder
containing 2024/holiday/clip.mp4 previews ok, converts, and lands at
mirror-out/2024/holiday/clip.mkv, confirmed with ffprobe as a real 160x120
h264 file rather than an empty one. Before the fix the same steps could not
get past the disabled Apply button.

158 tests pass, including that a scanned root satisfies validateJob and
resolves to the rebuilt directory - the seam where these two halves failed
to meet - plus the cross-drive null and drive-root cases, which were both
untested.
WillTDA and others added 30 commits October 1, 2026 21:03
…nd commit the lock file

The kit was a packed tarball while it was private. It's on npm now, and pinned to exactly 0.1.0-beta.1, so a fresh install can't move to a later beta before it's been tried here. Its code is the same as the tarball's, file for file.

package-lock.json is committed from now on, so every install and build gets the same versions, and CI can run npm ci.
… FFmpeg is missing

npm 12.1 no longer counts a version-pinned allowScripts entry (ffmpeg-static@5.3.0) as approving that package, though 5.3.0 is what's installed, so a fresh install on Linux never downloaded FFmpeg. The tests that need it skipped, and electron-builder built an AppImage and a .deb without it, saying nothing. The approvals are now by name, the form npm 12.1's own install-scripts approve writes; npm 12.0.2 runs them the same way (checked). electron@44.0.0 goes: Electron 44.4 has no install script.

CI now checks that FFmpeg and FFprobe are there after npm ci.
2.0.0-beta.3: on @diamonddigitaldev/electron-kit
…eps for Windows and Linux

package.json's description said "An Electron-based Windows app", and it's what the .deb and .rpm show as their description; it now matches the README's own tagline. The README's platform badge said Windows only, though 2.0 builds for Linux too.

Installation now has a part per system, as 2.0.0-beta.3's release notes do: the Windows installer and its SmartScreen note; the .deb, .rpm and AppImage, with what doesn't work on Linux yet; then what's bundled, what the updater's files are, and how to go back.
Call it a desktop app everywhere, and give the README installation steps for Windows and Linux
2.0.0-beta.4, on electron-kit 0.1.0-beta.3
…onverting card

- Cancel no longer flickers under the pointer: a card's buttons are only
  rebuilt when they change, not on every progress tick.
- The Last frame preview steps back when its seek lands after the video's
  last picture, as it does when the audio runs longer than the video.
- A card in a running batch, queued or converting, locks its Advanced
  Options button, its image-count line and its format select. The dialog
  used to open and report settings applied that never took effect.

2.0.0-beta.5, with its manual testing section.
Fix three card bugs, 2.0.0-beta.5
…ux, appId and busy

- electron-builder.cjs, from config(): package.json's build moves there, with the 22 types as fileTypes and
  "Convert with Diamond File Converter" on files and folders as contextMenu. installer.nsh goes: the kit's
  installer writes the same key, asks who it's for (upgrading an install for everyone in place), and asks for
  each type and the right-click entry. On Linux, the .deb and .rpm put the app in Open With for each type.
- start({ appId }): Windows groups the window under its pinned icon and shows its notifications.
- mountShell({ busy }): Restart Now asks first while a conversion or a join runs.
- README's installation, the design-system note, and a beta.6 section in MANUAL-TESTING.md.
2.0.0-beta.6: the kit's config() and installer, on electron-kit 0.1.0-beta.8
… ends

converting and joinRunning were cleared only when the run resolved; a run that threw left Restart Now asking about a conversion that wasn't running.
Once a card had converted, its format and Advanced Options could still be
changed, but nothing would run them: Convert skips a Done card, and only a
failed card offered Retry. A converted card now keeps the format and options
that made its file, locked as a card in the run is, with Convert Again beside
Show in Folder. Convert Again sends it back to Ready with the same format and
options, unlocked, and Convert runs only it. The earlier file is left where it
is: a run onto the same name meets File Already Exists, as any other does.

The lock, the reset and the card's buttons move into display.js, where the
suite can reach them. The bulk bar's format and Bulk Edit are disabled when
every card they would apply to is locked, rather than taking a pick and
changing nothing.
…'t delete it

Converting a file to its own format onto itself sent FFmpeg straight at its
own input, which it refuses ("cannot edit existing files in-place"). The
clean-up after a failed run then deleted the output path, which was the
original. Any overwrite whose run failed or was cancelled lost the old file
the same way.

A run onto something that already exists now writes name.dfc-partial.ext
beside it and renames it over the target only once FFmpeg exits cleanly.
Each clip's Trim button opens the span control under its row: the slider,
the exact-time boxes, the first and last frames, and Shift for one frame at a
time. The drag and keys (wireTrimHandles) and the frame painter
(framePreviewer) are now shared with Advanced Options rather than copied.
Restart Now's busy question is cleared however a conversion or a join ends
Convert Again, overwrite that can't delete the file, and Join's trim (2.0.0-beta.7)
…t runs

The clip list goes inert and grey, and Join into, File name and Browse are disabled, until the join ends however it ends. A file dropped on Join mid-run isn't added.
The lock made the whole scrolling area inert, which took its scrollbar and mouse wheel with it, so a long list couldn't be looked through mid-join. Now the clips and Add More are inert and grey; the scroller stays live.
Join's clip list scrolls while a join runs
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